Laden...

async -> sync | ASP.NET - *Sicher* synchron auf einen Task warten

Letzter Beitrag vor 3 Monaten 18 Posts 587 Views
async -> sync | ASP.NET - *Sicher* synchron auf einen Task warten

Hallo zusammen,

ich habe das vor, was man eigentlich nicht tun sollte, ich weiß 😃

Ich habe ein ziemlich altes ASP.NET-Projekt (nicht ASP.NET Core) mit .NET 4.6.2, das von vorne bis hinten synchron arbeitet.
Das zu ändern kommt auch nicht in Frage, aber ich muss asynchronen Code darin aufrufen.
Das ganze läuft auf einem IIS.

Der betreffende Code kann sehr oft gleichzeitig laufen, also z.B. sehr viele Nutzer, die ein bestimmtes Datum abwarten und dann alle auf einmal los rennen, Requests produzieren und ThreadPool-Threads füllen. Die Requests laufen auch recht lange, es kann also durchaus sein, dass sehr viele ThreadPool-Threads (oder ggf. sogar alle) gleichzeitig belegt sind.
Die generelle Performance ist natürlich nicht egal, aber zweitrangig, hauptsache es läuft und es gibt keinen Deadlock.

Wenn ich nun auf asynchronen Code mit .Wait() warte, dann sorgt das bei ASP.NET ja für einen Deadlock. Das kann ich wie folgt umgehen:

var syncContext = SynchronizationContext.Current;

try
{
    SynchronizationContext.SetSynchronizationContext(null);

    myAsyncMethod().Wait();
}
finally
{
    SynchronizationContext.SetSynchronizationContext(syncContext);
}

Aber bei dieser Arbeitsweise sehe ich das Risiko, dass es trotzdem zu einem (quasi-) Deadlock kommen kann, wenn zu viele ThreadPool-Threads belegt sind, die alle darauf warten, dass Platz auf dem ThreadPool frei wird, damit der Task laufen kann.

Ich habe gelesen, dass folgendes ausreichen soll:

Task.Run(() => myAsyncMethod()).Wait();

Mir ist aber nicht klar, wie das ausreichen soll? Der neue Task geht doch auch über den Default oder Current TaskScheduler und damit auf den ThreadPool.
Stattdessen würde ich folgendes nutzen:

Task.Factory.StartNew(() => myAsyncMethod(), TaskCreationOptions.LongRunning);

So wie ich das verstehe, bekommt der Task dann VIELLEICHT einen komplett neuen Thread und muss nicht auf den ThreadPool warten.
Aber reicht das aus? Und das würde ja auch bedeuten, dass sehr viele neue Threads erstellt werden müssen.

Meine Überlegung war daher, einen eigenen TaskScheduler zu schreiben, dessen Instanz für die Dauer eines Request intern einen Thread in einer Endlosschleife nutzt, um die Tasks abzuarbeiten. Auf den Task warte ich dann mit RunSynchronously(TaskScheduler). Da die Requests synchron arbeiten, würde hier ein Thread ausreichen und ich habe effektiv alles, was einen Deadlock verursachen kann, aus dem Spiel genommen.

Würde das ausreichen, um ein Deadlock zuverlässig zu verhindern?
Und kann ich das irgendwie automatisiert testen?

Ich habe versucht einen UnitTest zu schreiben, der alle ThreadPool-Threads füllt und so diesen Zustand künstlich herstellt.
Der Test funktioniert auch, aber nur manchmal, vermutlich weil im Hintergrund immer sporadisch wieder Threads frei werden und meinen Test kaputt machen.
Meine andere Idee wäre, ein kleines Programm zu schreiben, was das isoliert in einem eigenen Prozess testet, ohne dass irgendetwas anderes Threads braucht. Dieses Programm kann ich dann aus dem "UnitTest" heraus aus starten und das Ergebnis abfangen.
Wirklich richtig klingt das auch nicht, aber ich würde gerne sicherstellen, dass es keinen Deadlock geben kann.

Habt Ihr dazu Tipps, oder habe ich einen Verständnisfehler?

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Der Autor der Antwort auf Stackoverflow schreibt doch selber:

However, all three methods cause the potential for deadlock and thread pool starvation issues.

Das löst mein Problem nicht, das ist es doch gerade, was ich verhindern will.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

.GetAwaiter().GetResult() ist Wait() auf reines await-Replacement prinzipiell vorzuziehen, weil zB potentielle Exceptions innerhalb eines Wait() ansonsten zu einem Deadlock führen kann.
Jedoch kann .GetAwaiter().GetResult() in ganz gewissen Szenarien immer noch zu Deadlocks führen. Umgekehrt ausgedrückt gibt es nur ganz arg wenige Szenarien, die überhaupt Deadlock-frei auf Tasks "warten" können.


Nur der Vollständigkeit halber: hab aktuell keine vollständige Lösung auf Deine Frage.

Ich hab jetzt eine erste Version des TaskSchedulers.
Die UnitTests (naja, eher Integration / Ende-zu-Ende-Tests), die ich damit auf synchron umgebaut habe, laufen durch.

Aber ich werde das Gefühl nicht los, dass das ganze eine sehr wackelige Angelegenheit ist.
Und Deadlocks testen kann ich immer noch nicht, ich kann nur versuchen, einige Tests für den TaskScheduler zu schreiben.

internal sealed class SingleThreadTaskScheduler : TaskScheduler, IDisposable
{
    private readonly ManualResetEventSlim _workerThreadPausedEvent = new(false);
    private readonly CancellationTokenSource _disposedCancelTokenSource = new();
    private readonly ConcurrentQueue<Task> _tasksQueue = new();
    private readonly Thread _workerThread;

    public override int MaximumConcurrencyLevel => 1;

    public SingleThreadTaskScheduler(string workerThreadName)
    {
        _workerThread = new Thread(processTasks)
        {
            IsBackground = true,
            Name = workerThreadName,
        };
    }

    protected override IEnumerable<Task> GetScheduledTasks() => _tasksQueue.ToArray();

    protected override void QueueTask(Task task)
    {
        if (_disposedCancelTokenSource.IsCancellationRequested)
            throw new ObjectDisposedException(GetType().Name);

        if (_workerThread.ThreadState.HasFlag(ThreadState.Unstarted))
            _workerThread.Start();

        _tasksQueue.Enqueue(task);
        _workerThreadPausedEvent.Set();
    }

    protected override bool TryDequeue(Task task) => false;

    protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
    {
        if (Thread.CurrentThread.ManagedThreadId == _workerThread.ManagedThreadId)
            return TryExecuteTask(task);

        return false;
    }

    private void processTasks()
    {
        while (!_disposedCancelTokenSource.IsCancellationRequested)
        {
            try
            {
                if (_tasksQueue.TryDequeue(out var task))
                {
                    TryExecuteTask(task);
                }
                else
                {
                    _workerThreadPausedEvent.Wait(_disposedCancelTokenSource.Token);
                    _workerThreadPausedEvent.Reset();
                }
            }
            catch (Exception)
            {
            }
        }
    }

    public void Dispose()
    {
        if (_disposedCancelTokenSource.IsCancellationRequested)
            return;

        _disposedCancelTokenSource.Cancel();

        while (_tasksQueue.TryDequeue(out _))
            ;

        _workerThreadPausedEvent.Set();

        if (!_workerThread.ThreadState.HasFlag(ThreadState.Unstarted))
        {
            _workerThread.Join(1000);
            _workerThread.Abort();
        }

        _disposedCancelTokenSource.Dispose();
        _workerThreadPausedEvent.Dispose();
    }
}

Der wird wie folgt genutzt:

public MyResult DoWork(MyArgs args, CancellationToken cancellationToken = default)
    => execute(() => _asyncVersion.DoWorkAsync(args, cancellationToken), cancellationToken);

private void execute(Func<Task> executeAsync, CancellationToken cancellationToken)
    => execute(executeAsync, static t => t.Unwrap(), cancellationToken);
private TResult execute<TResult>(Func<Task<TResult>> executeAsync, CancellationToken cancellationToken)
    => execute(executeAsync, static t => t.Unwrap(), cancellationToken).Result;

private TTask execute<TTask>(Func<TTask> executeAsync, Func<Task<TTask>, TTask> unwrap, CancellationToken cancellationToken)
    where TTask : Task
{
    var allowedThreadId = Interlocked.CompareExchange(ref _allowedThreadId, Environment.CurrentManagedThreadId, 0);

    if (allowedThreadId != 0 && allowedThreadId != Environment.CurrentManagedThreadId)
        throw new InvalidOperationException("Only one thread is allowed to use this API.");

    var wrapperTask = Task.Factory.StartNew(executeAsync, cancellationToken, TaskCreationOptions.None, _taskScheduler);

    var task = unwrap(wrapperTask);

    task.GetAwaiter().GetResult();

    return task;
}

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Ich hab nen bisschen rum probiert, aber keine funktionierende Lösung in meinem Zeitblock gefunden.
Aber eigentlich bin ich der Meinung, dass das mit Thread.QueueUserWorkItem bzw. Thread.UnsafeQueueUserWorkItem funktionieren müsste.

  • ThreadPool.QueueUserWorkItem(State); State = Params + CTS.Token
  • Warten bis der Thread sich beendet / CTS gecancelt wird = Vorgang ist beendet
  • Rückgabe über den State erhalten

Hm - nach dem, wie ich die Doku dazu verstehe, wäre UnsafeQueueUserWorkItem das, was ich nutzen müsste, da hier kein ExecutionContext weitergegeben wird - soweit so gut, damit fällt zumindest schon einmal der SynchronizationContext weg, das muss ich dann nicht mehr manuell tun.

Aber habe ich damit nicht immer noch das Problem, dass ein Deadlock auftreten kann, wenn alle ThreadPool-Threads die Ausführung je eines Tasks mit UnsafeQueueUserWorkItem in die ThreadPool-Queue legen, die aber nie ausgeführt werden, weil alle ThreadPool-Threads darauf warten?

Das wäre ein Extrembeispiel, aber dennoch möglich, oder übersehe ich etwas?

Ich hab deinen Vorschlag Mal ausprobiert:

public void Cancel(BereitstellungType type, Guid id, CancellationToken cancellationToken = default)
    => execute((type, id, cancellationToken), static (api, x) => api.CancelAsync(x.type, x.id, x.cancellationToken), cancellationToken);

private void execute<TState>(TState state, Func<IMyAsyncApi, TState, Task> executeAsync, CancellationToken cancellationToken)
    => executeCore(state, executeAsync, cancellationToken);
private TResult execute<TState, TResult>(TState state, Func<IMyAsyncApi, TState, Task<TResult>> executeAsync, CancellationToken cancellationToken)
    => executeCore(state, executeAsync, cancellationToken).Result;
private TTask executeCore<TTask, TState>(TState state, Func<IMyAsyncApi, TState, TTask> executeAsync, CancellationToken cancellationToken)
    where TTask : Task
{
    var closure = new Closure<TTask, TState>()
    {
        Api = _asyncApi,
        State = state,
        ExecuteAsync = executeAsync,
    };

    var enqueued = ThreadPool.UnsafeQueueUserWorkItem(
        callBack: static state => ((Closure<TTask, TState>)state).Execute(),
        state: closure);

    if (!enqueued)
        throw new InvalidOperationException("Could not enqueue work item.");

    closure.Wait(cancellationToken);

    closure.ExecutedTask.GetAwaiter().GetResult();

    return closure.ExecutedTask;
}

private sealed class Closure<TTask, TState> : ManualResetEventSlim
    where TTask : Task
{
    private TTask? _executedTask;

    public required IMyAsyncApiApi { get; init; }
    public required TState State { get; init; }
    public required Func<IMyAsyncApi, TState, TTask> ExecuteAsync { get; init; }

    public TTask ExecutedTask
    {
        get => _executedTask ?? throw new InvalidOperationException("Task has not been executed yet.");
    }

    public void Execute()
    {
        _executedTask = ExecuteAsync(Api, State);

        _executedTask.GetAwaiter().GetResult();

        Set();
    }
}

Die Tests laufen durch, das ist schon Mal gut.
Aber ist es auch Deadlock-Sicher? Das ist ja das, was mir so Kopfschmerzen bereitet, ans Laufen bekommen ist leicht, aber sicher gegen Deadlocks machen, das ist unangenehm 😕 Und wenn ich etwas übersehe, dann brennt irgendwann in vielleicht 5 Jahren die Luft und niemand weiß, wo oben und unten ist.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Aber habe ich damit nicht immer noch das Problem, dass ein Deadlock auftreten kann, wenn alle ThreadPool-Threads die Ausführung je eines Tasks mit UnsafeQueueUserWorkItem in die ThreadPool-Queue legen, die aber nie ausgeführt werden, weil alle ThreadPool-Threads darauf warten?

Ja, aber das ist ja ein grundlegendes Problem des Henne-Ei Prinzips.

Aber mit

Der betreffende Code kann sehr oft gleichzeitig laufen, also z.B. sehr viele Nutzer, die ein bestimmtes Datum abwarten und dann alle auf einmal los rennen, Requests produzieren und ThreadPool-Threads füllen. Die Requests laufen auch recht lange, es kann also durchaus sein

gehst Du ja bewusst dieses Risiko ein. Daher skaliert man ja zB Ticket-Sale-Systeme auch nicht über diese Art und Weise. Das ist im Endeffekt ja nichts anderes als ein DoS gegen sich selbst.

Der Standard-ThreadPool bei WIn32 hat 1023 Worker und 1000 für async I/O. Musst halt das Erstellen limitieren / extern skalieren. Aber das wäre auch ohne den Pool der Fall, sprich bei manuellem Handling.


Der ThreadPool is nen sehr komplexes und schlaues Konstrukt; vielleicht kann er das ein oder andere auch erkennen. Aber für Long-Running IO ist eigentlich der ThreadPool das falsche Mittel bzw. ist das eigentlich nicht seine beste Stärke. In Deinem Fall is eventuell doch eigenes Management statt Pool der bessere Weg.

In Deinem Fall is eventuell doch eigenes Management statt Pool der bessere Weg.

Zu der Schlussfolgerung bin ich auch gekommen.
Wenn das Ausführen der Tasks auf dem ThreadPool unvermeidbar ein Deadlock-Risiko birgt, ist die einzig logische Konsequenz: Kein ThreadPool ^^

Wobei die Tasks selber ziemlich schnell laufen sollten, der "äußere" Request-Thread ist das, was langsam ist, daran kann ich aber im Moment nichts ändern.

Mein TaskScheduler oben scheint ja zu laufen, aber ist der sicher? Siehst Du da Lücken?
Oder produziere ich mir damit ein weiteres Deadlock-Risiko, weil es nur ein Thread ist?
So wie ich es verstehe, ist ja jeder Task-Step beenden, bevor der nächste Step an den TaskScheduler übergeben wird, also sollte das kein Problem sein.
Die Alternative wäre, mehrere Threads zu verwalten, also im Prinzip ein eigener ThreadPool - schade, dass der .NET-ThreadPool komplett statisch arbeitet und ich mir keine eigene Instanz bauen kann 😕
Ein Task.Wait() aus meinem Worker-Thread führt natürlich zu einem Deadlock, aber das ist klar und kann ich ggf. mit einem Check verhindern, ob die Queue beim Enqueue leer ist.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Zitat von Palladin007

schade, dass der .NET-ThreadPool komplett statisch arbeitet und ich mir keine eigene Instanz bauen kann 😕

Der ThreadPool ist im Endeffekt nichts anderes als eine Implementierung des TaskScheduler auf einer eigenen Ebene. Davon eine eigene Instanz zu erzeugen ist ein potentielles Sicherheitsrisiko. Daher soll der ThreadPool auch nicht verwendet werden, wenn Du sicherheitsrelevante Aufgaben hast und Dein ausführende System keine Zugriffssicherheit des ThreadPools bietet (Du zB das System mit anderen Anwendungen auf der Ebene teilst).

Deine Implementierung SingleThreadTaskScheduler ist also quasi ein Custom Thread Pool. Ich sehe


Kurze Frage zur Problemstellung:

Sind die Aufgaben in den Tasks, auf die Du wartest, identisch?

Also Fragen mehrere Requests zB an was "1+1" ist und Du hast dann ein externen Call, der die "2" liefert?
Kannst Du dann nicht die Anfrage zusammen mit dem Task in einem Dict halten, sodass gar kein zweiter Task erzeugt wird, der nochmal "1+1" beinhaltet, sondern Du hast ja dann den Task schon auf den Du wartest?

e.g. dict.Add("1+1", myTask);

Dann hättest Du die Race-Condition weg-optimiert.

Davon eine eigene Instanz zu erzeugen ist ein potentielles Sicherheitsrisiko.

Das verstehe ich nicht? Angenommen, der ThreadPool würde nicht statisch arbeiten, sondern alle Threads je Instanz verwalten, wo ist dann das Sicherheitsrisiko? Die ThreadPool-Klasse hätte dann eine Current-Property und man kann seine eigene Instanz mit eigenen Threads erzeugen, das würde mein Problem auf einen Schlag aus der Welt schaffen.

Oder willst Du darauf hinaus, dass der ThreadPool von Windows verwaltet wird (es gibt ja eine "UseWindowsThreadPool"-MSBuild-Property) und damit zwei vollständig getrennte ThreadPool-Instanzen gar nicht möglich sind?

Deine Implementierung SingleThreadTaskScheduler ist also quasi ein Custom Thread Pool. Ich sehe

Das sieht aus, als würde da etwas fehlen? 😃

Sind die Aufgaben in den Tasks, auf die Du wartest, identisch?

Cachen ist leider nicht möglich. Es wird kein Ergebnis errechnet, sondern eine externe Komponente gesteuert und überwacht.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Zitat von Palladin007

Das verstehe ich nicht? Angenommen, der ThreadPool würde nicht statisch arbeiten, sondern alle Threads je Instanz verwalten, wo ist dann das Sicherheitsrisiko?

Ich hatte in Erinnerung, dass es einen generellen Security Flaw gibt, wenn eigene Thread Pools abgeleitet werden können aufgrund der Implementierungsart, aber das scheint nur (noch?) für UnsafeQueueUserWorkItem Code Access Security bei zu gelten, siehe Remarks.

Das sieht aus, als würde da etwas fehlen? 😃

Hups. Kommt davon wenn man nebenher aufgefordert wird "mal kurz" was zu machen...

Ich sehe generell beim Drüberschauen keine große Lücke, auf Anhieb.

Hallo Palladin007,

ganz hab ich das eigentliche Problem nicht verstanden. Kannst du das etwas allgemeiner beschreiben?
Bisher sind die Möglichkeiten schon sehr in eine Richtung getrieben, aber mein Gefühl mein dass es da eine andere Möglichkeit geben sollte.

z.B. sehr viele Nutzer

Lässt sich das größenordnungsmäßig angeben?

Der betreffende Code kann sehr oft gleichzeitig laufen, also z.B. sehr viele Nutzer, die ein bestimmtes Datum abwarten und dann alle auf einmal los rennen, Requests produzieren und ThreadPool-Threads füllen. Die Requests laufen auch recht lange,

Warten die alle auf das gleiche bestimmte Datum od. jeder für sein eigenes od. gibt es Gruppen von Datums auf die gewartet wird?

Müssen die Requests warten od. kann z.B. via SignalR das nur 1x laufen und die Benachrichtigungen, etc. werden dann zu den Clients gesendet?

UnitTest zu schreiben, der alle ThreadPool-Threads füllt

Da der ThreadPool in .NET (seit .NET 4) adaptiv arbeitet (mit einem Hill-Climbing Algorithmus) um so die Anzahl der Threads an die Workload anzupassen, bringt so ein Test recht wenig.
Außer die Min-/Max-Threadzahl wird begrenzt, aber da handelst du dir womöglich an anderer Stelle ein Problem ein.

IIRC verwendet der IIS pro Request einen (Windows-) Thread (ob dieser vom Windows ThreadPool stammt od. nicht weiß ich nicht), somit ist das Beschränken auf letztlich einen Thread so od. so eher fraglich ohne massive Skalierungsproblem zu haben und gleichzeitig die Gefahr von latenten Deadlocks groß.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

ganz hab ich das eigentliche Problem nicht verstanden. Kannst du das etwas allgemeiner beschreiben?

Es geht um eine mit .NET 4.6.2 entwickelte ASP.NET-Anwendung, die intern von vorne bis hinten synchron arbeitet.
Und es geht um ein Framework, das asynchron arbeitet, was aus der alten Anwendung heraus genutzt werden muss.

Die betreffenden Codesytellen auf asynchron umstellen wäre viel zu aufwändig, da das ungefähr die komplette Anwendung betreffen würde.
Es bleibt also die Herausforderung, aus synchronen Code heraus synchron auf asynchronen Code zu warten - was natürlich ein Problem ist.

Ich sehe dabei zwei Deadlock-"Arten":

  1. Der "klassische" Deadlock, wie man ihn ja sehr simpel mit Task.Wait ohne irgendwelche Vorkehrungen verursachen kann.
  2. Alle ThreadPool-Threads wollen synchron auf je einen Task warten, diese Tasks bekommen aber keinen Platz auf dem ThreadPool, weil jeder ThreadPool-Thread auf "seinen" Task wartet.

Bisher hebe ich zwei Möglichkeiten auf dem Schirm:

  1. Den asynchronen Code ohne SynchronizationContext oder ExecutionContext auf den ThreadPool legen und dort ausführen und auf die Beendigung warten, entweder mit Task.Factory.StartNew() und TaskScheduler.Default oder Thread.UnsafeQueueUserWorkItem().
    Zwischenfrage: Gibt es da relevante Unterschiede zwischen beiden, wenn das Ziel nur ist, keinerlei Context-Informationen durchreichen zu lassen?
  2. Den Code in einem eigenen Worker-Thread mit Hilfe eines eigenen TaskSchedulers + SynchronizationContext (sofern nötig, das teste ich noch) und Task.Factory.StartNew() ausführen.

Beides würde dazu führen, dass ich synchron auf die Beendigung des asynchronen Codes warten kann, aber beides birgt auch ein gewisses Deadlock-Risiko, das ich nicht einschätzen kann - hoffentlich Du? 😄

Lässt sich das größenordnungsmäßig angeben?

Schwer, da es - wie ich eben erfahren habe - doch nicht nur eine Funktion ist, sondern so gut wie alle Funktionen.
Die Schätzung ist aber MAXIMAL 8000 (eher sehr viel weniger) gleichzeitiger Nutzer, was an sich nicht viel ist, aber es reicht ja schon, wenn nur wenige Nutzer ungünstig gleichzeitig etwas tun wollen.

Warten die alle auf das gleiche bestimmte Datum od. jeder für sein eigenes od. gibt es Gruppen von Datums auf die gewartet wird?

Meine ursorüngliche Aussage, dass sie auf ein Datum warten, war falsch.
Ich können es tatsächlich nicht konkret sagen, wir haben leider auch kein brauchbares Logging, was etwas darüber verraten könnte.
Aber in der Regel sieht es so aus, dass ab einem Datum ein Prozess aus Sicht des Nutzers frei gegeben wird und dann können die Nutzer los legen. Die Nutzer haben dann aber kein Limit (z.B. max. X Tickets verfügbar) und sie haben ein Zeitfenster von 2 Monaten, in dem sie arbeiten können.
Es kann also sein, dass viele Nutzer direkt zu Beginn los rennen, oder ganz zum schluss nochmal in letzter Sekunde. Wahrscheinlicher ist aber, dass es sich über die 2 Monate verteilt.

Und es gibt natürlich auch die wage DDoS-Gefahr.
Ein schlecht skallierender Webserver ist da natürlich sowieso ein Problem, aber ich muss ja nicht noch ein potentielles Deadlock-Risiko drauf legen ^^

Müssen die Requests warten od. kann z.B. via SignalR das nur 1x laufen und die Benachrichtigungen, etc. werden dann zu den Clients gesendet?

Du meinst, die Kommunikation z.B. in die DB legen und dann in einem Hintergrund-Thread abarbeiten?
Das funktioniert leider auch nicht, weil die Requests die Antwort oder ggf. Fehler-Informationen selber sofort brauchen.
Aufsplitten, dass das Frontend erst später informiert wird, kommt leider auch nicht in Frage.

IIRC verwendet der IIS pro Request einen (Windows-) Thread (ob dieser vom Windows ThreadPool stammt od. nicht weiß ich nicht), ...

Das - ob der Thread vom ThreadPool stammt - wäre ggf. ein entscheidendes Detail?
Eine Annahme von mir für ein potentielles Deadlock-Risiko war ja, dass alle ThreadPool-Threads beschäftigt sind, indem sie Tasks auf den ThreadPool ausführen wollen.

Wenn die Requests aber gar nicht auf dem ThreadPool ausgeführt werden, dann würde der problematische Code gar nicht auf dem ThreadPool laufen, also fällt dieses Risiko weg.

..., somit ist das Beschränken auf letztlich einen Thread so od. so eher fraglich ohne massive Skalierungsproblem zu haben und gleichzeitig die Gefahr von latenten Deadlocks groß.

Meinst Du damit die Tatsache, dass die Requests alle synchron arbeiten und damit über die ganze Laufzeit diesen einen Thread blockieren, obwohl eigentlich 99% der Zeit nur auf IO gewartet werden muss?
Das ist mir bewusst, ändern kann ich das aber leider nicht.

Oder meinst Du, dass ich den CustomTaskScheduler mit nur einem Thread implementiert habe?
Das basierte auf der Annahme, dass es im Großen und Ganzen sowieso nie mehr als ein Task gleichzeitig (pro Request) sein wird, also würde ein Thread + eine Queue dafür ausreichen. Es könnte zwar sein, dass irgendwo mehrere Tasks parallel gestartet werden, aber solange mit await und nicht mit Wait() darauf gewartet wird, ist das ja kein Problem. Der Code läuft dann zwar langsamer, aber das ist vermutlich nicht weiter relevant.

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007,

ich hab in meiner Antwort unten auf deine Punkte / Frage reagiert, fürchte aber dass wir so nicht weiterkommen.

Daher beschreib bitte den zeitlichen Ablauf wann was passieren soll. Zuerst einmal für einen Benutzer.
Dann wie es für mehrere Benutzer ausschaut.

Berücksichtige dabei bitte auch, dass wir keine Ahnung von deinem Projekt haben (du jedoch hoffentlich schon 😉).


Alle ThreadPool-Threads wollen synchron auf je einen Task warten, diese Tasks bekommen aber keinen Platz auf dem ThreadPool, weil jeder ThreadPool-Thread auf "seinen" Task wartet.

Auch unter .NET 4.x funktioniert so der ThreadPool nicht. Wenn der TP merkt dass "zuwenig weitergeht", so werden mehr Threads injiziert.

Wenn die Annahme von max. 8000 gilt, so wären das max. 8000 Threads und das ist bei einer CPU mit wesentlich weniger Kernen wohl nicht zielführend (der arme OS-Scheduler soll ja schauen dass die Arbeiten auf die CPUs möglichst fair verteilt werden).

entweder mit Task.Factory.StartNew() und TaskScheduler.Default oder Thread.UnsafeQueueUserWorkItem().
Zwischenfrage: Gibt es da relevante Unterschiede zwischen beiden, wenn das Ziel nur ist, keinerlei Context-Informationen durchreichen zu lassen?

Bei ersterem hast du halt einen Task, bei letzterem nicht.
D.h. Status, Fehlerbehandlung ist mit dem Task meist einfacher. Ebenso falls Continuations benötigt werden hat der Task seine Vorteile und wurde ursprünglich genau wegen solcher Dinge auch eingeführt.

Meine ursorüngliche Aussage, dass sie auf ein Datum warten, war falsch.
Ich können es tatsächlich nicht konkret sagen, wir haben leider auch kein brauchbares Logging, was etwas darüber verraten könnte.
Aber in der Regel sieht es so aus, dass ab einem Datum ein Prozess aus Sicht des Nutzers frei gegeben wird und dann können die Nutzer los legen

Da bin ich bisher noch nicht mit Infos gesegnet 😉
Weiter oben steht

Es wird kein Ergebnis errechnet, sondern eine externe Komponente gesteuert und überwacht.

Was nun?
Wird eine Komponenten überwacht und wenn Ereignis X eintritt, so gehts los?

Oder ist mit Datum wirklich ein Zeitpunkt gemeint, andem ein Benutzer los legen kann?
Warum dann der Request bereits vorher und dieser soll auf das Datum warten?

und sie haben ein Zeitfenster von 2 Monaten

Was passiert in diesem Zeitfenster, v.a. in Hinsicht auf die eingangs (OT) Requests?

Mir fehlt, auch jetzt noch, eine Beschreibung was das Ziel der Sache sein soll.
Wie lange dauern die Requests i.d.R., usw.

Es gibt jede Menge an Sequentialisierung-, Skalarsierung-, etc. Code, aber um das passende Verfahren auszuloten, sollte bekannt was erwünscht ist.

Das funktioniert leider auch nicht, weil die Requests die Antwort oder ggf. Fehler-Informationen selber sofort brauchen.

Das Problem gibt es so gut wie immer. Wobei auf was bezieht sich "sofort"?
Wenn die Request u.U. lange warten, dann ist "sofort" auch schon wieder später.
Warum ist ein "Ablegen" der Anfrage via HTTP, Bearbeitung und dann asynchrone (hier nur zeitlich gemeint) Rückmeldung via SignalR nicht möglich? Das kann sogar eher "sofort" durchgeführt werden, da das System insgesamt weniger unter Last steht und nicht >> Threads benötigt werden.

Aufsplitten, dass das Frontend erst später informiert wird, kommt leider auch nicht in Frage.

Warum nicht? Das wäre gängige Praxis und lässt sich auch mit ASP.NET (nicht Core) umsetzen.

Eine Annahme von mir für ein potentielles Deadlock-Risiko war ja, dass alle ThreadPool-Threads beschäftigt sind, indem sie Tasks auf den ThreadPool ausführen wollen.

Ein guter TP fügt dann einfach mehr Threads hinzu -- und entfernt diese ev. später wieder wenn sie nicht mehr benötigt werden.
Das Verhalten vom Windows-ThreadPool kenn ich aber nicht, nur jenes vom .NET ThreadPool.
Ein Deadlock ist da eher unwahrscheinchlich, eher Thread-Starvation od. Thread-Exhaustion, also dass die CPU vor lauter Threads kaum mehr wirklichen Fortschritt macht od. dass eben keine Threads mehr verfügbar sind.
Daher auch meine Intention zu erfahren was passiert, damit mit möglichst wenigen Threads das gelöst werden kann.

Meinst Du damit die Tatsache, dass die Requests alle synchron arbeiten und damit über die ganze Laufzeit diesen einen Thread blockieren, obwohl eigentlich 99% der Zeit nur auf IO gewartet werden muss?

Wenn auf IO sync gewartet wird, so ist dieser Thread eben blockiert bis IO fertig ist.
Auf IO kann jedoch async gewartet werden, dann wird kein Thread blockiert. Erst wenn IO fertig ist, so wird über den sog. IO-Completionport (ein Windows-Konzept, das es z.B. in Linux so nicht gibt / nicht verbreitet ist) einem im IO-ThreadPool verfügbaren Thread signalisiert dass er mit der "Continuation" (also ab nun dem folgenden Benutzercode) fortfahren kann.
Sind hier jedoch sehr viele (max. 8000?) Continuations registriert, so gibt es eine Menge zu tun. Da bin ich mir sicher, dass diese anders gelöst werden kann.

Oder meinst Du, dass ich den CustomTaskScheduler mit nur einem Thread implementiert habe?

Ich hab mir den Code (mangels Zeit) nicht angeschaut.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Ich kenne das Projekt leider auch nicht ^^
Ich sitze quasi zwischen den Tischen.

Aber so weit ich das verstehe:

Das ganze Thema soll Benachrichtigungen versenden, das können entweder E-Mails sein, oder andere Wege.
Mir geht's dabei um so einen anderen Weg, das läuft das dann über einen Dienst, auf den ich nur bedingt (kaum) Einfluss habe.
Dieser externe Dienst nimmt (über eine Web-API) Daten entgegen, validiert ziemlich viel und teilt mir mit, ob alles ok ist, oder nicht.
Ich bin derjenige, der den intern genutzten C#-Client dafür entwickelt.

Deshalb kann ich auch nicht sicher sagen, wie der Ablauf konkret sein wird: Es ist effektiv alles, überall, wo Benachrichtigungen raus gehen.
Und das ist auch der Grund, warum wir keine grundlegenden Strukturen (sync->async oder SignalR) umbauen können, wir müssten effektiv das ganze Projekt umbauen.
Und deshalb weiß ich auch keine Größenordnungen. Die 8000 sind eine Schätzung, die man mir mitgeteilt hat - in der Realität wahrscheinlich sehr viel weniger, die 8000 wären dann also eher ein absoluter Ausnahmefall und solange das irgendwie trotzdem verarbeitet werden kann, auch wenn es langsam ist, bin ich prinzipiell fein damit.

Die komplexen Validierungen sind auch der Grund, warum ich immer sofort eine Antwort brauche. Die Daten, die da validiert werden, kommen nicht (nur) von uns, wir können sie also nicht einfach korrigieren, wenn was schief ist und einen zweiten Versuch starten. Wir müssen die Arbeit also abbrechen und Fehlerinformationen anbieten können, wenn es ein Problem gibt. Würden wir die Nachrichten erst in die DB schreiben und irgendwann später versenden, hätten wir diese Möglichkeit nicht mehr, oder wir müssten sämtliche Prozesse grundlegend umstrukturieren.
Oder, wenn es um Status-Updates geht, dann brauchen wir natürlich den Status der Benachrichtigungen, um dann darauf reagieren zu können.

Das Zeitfenster von zwei Monaten kommt daher, dass es Prozesse gibt, an die sich die Anwender halten müssen. Ab Datum X wird ein Prozess freigegeben und für zwei Monate können die Anwender dann an diesem Prozess arbeiten (z.B. einen Antrag stellen), danach wird's dann wieder zu gemacht. Es kann also sein, dass sie sofort ab Datum X los laufen, oder erst 6 Wochen später, oder ein Tag vor Deadline.

Die Kommunikation findet über das Netzwerk statt und das ist - nach meinem Wissen - bisher nur mit dem veralteten WebClient möglich und auch da war's ja eher eine Hack, als eine vernünftige Lösung. Die gleiche Code-Basis soll aber auch in anderen modernen Projekten genutzt werden, ich möchte also möglichst vermeiden, mir eine veraltete Technologie ans Bein zu binden, für ein Projekt, was vermutlich eh in ein paar Jahren grundlegend modernisiert wird.

Ich habe also die Wahl:

  1. Alles doppelt bauen, einmal WebClient, einmal HttpClient - ggf. kann ich da mit MultiTargeting und Precompiler-Direktiven helfen, aber blöd ist's trotzdem
  2. Für dieses eine Projekt eine async->sync-Krücke, die ich dann später wieder abstoßen kann - solange es kein Deadlock-Risiko gibt.

Mir fällt es schwer, abzuschätzen, wie groß das Risiko bei Option 2 ist, das versuche ich hier herauszufinden.
Deine Klarstellungen zum ThreadPool hat aber eines erreicht: Meine Annahme zur 2. "Deadlock-Art" war falsch, das Risiko fällt also weg.

Dass die generelle Performance bei vielen gleichzeitigen Requests schlecht sein könnte, das ist verkraftbar. Das kommt vermutlich sowieso nicht so oft vor. Die Anwendung hat generell eine schlechte Performance, es gibt allso Mechanismen, die das behandeln ^^
Entscheidend ist nur, dass die Anwendung weiter läuft, auf gar keinen Fall darf sich alles aufhängen.

Mein Ziel mit dieser Frage ist also teils eine brauchbare Lösung für sync->async zu finden (auch für mich für den Lerneffekt) und um einzuschätzen, welche der beiden Optionen besser ist: WebClient vs. async->sync-Krücke.


Aktuell - maßgeblich durch deine Klarstellung zum ThreadPool - tendiere ich dazu, mit Task.Factory.StartNew einen neuen Task auf dem ThreadPool zu erstellen und synchron darauf zu warten. Damit habe ich das Deadlock-Risiko durch ASP.NET umgangen und bzgl. die Befürchtung, dass es ein zweites Deadlock-Risiko gibt, hast Du aus der Welt geschafft.

Also so:

private TResult execute<TResult>(Func<Task<TResult>> executeAsync, CancellationToken cancellationToken)
{
    var task = Task.Factory.StartNew(
        function: static x => ((Func<Task<TResult>>)x)(),
        state: executeAsync,
        cancellationToken,
        TaskCreationOptions.None, // Wäre hier eine Option sinnvoll? HideScheduler?
        TaskScheduler.Default
    ).Unwrap();

    return task.GetAwaiter().GetResult();
}

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.

Hallo Palladin007,

da hat sich jemand eine sehr zukunftsträchtige Architektur überlegt und du bist jetzt in der bescheidenen Situation da etwas zu implementieren.

Ein paar Infos sind mehr vorhanden, aber es fehtl noch viel für ein gutes Bild worum es im Grunde geht.

Dieser externe Dienst nimmt (über eine Web-API) Daten entgegen, validiert ziemlich viel und teilt mir mit, ob alles ok ist, oder nicht.

Wann kommen dabei die eingangs genannten Requests (via IIS) ins Spiel?
Hat jeder Benutzer hier seine eigenen Daten od. sind die Daten für viele / alle Benutzer die gleichen?

Die 8000 sind eine Schätzung, die man mir mitgeteilt hat - in der Realität wahrscheinlich sehr viel weniger, die 8000 wären dann also eher ein absoluter Ausnahmefall

Es sollte schon auch vom Worst-Case ausgegangen werden. Oder zumindest von einer 90% Häufigkeit, od. dgl., andernfalls kann das Problem nicht sauber betrachtet werden.

Die komplexen Validierungen sind auch der Grund, warum ich immer sofort eine Antwort brauche

Was heißt "sofort" (siehe vorhin)?
Wohin wird die Antwort geschickt?

Wir müssen die Arbeit also abbrechen und Fehlerinformationen anbieten können

Welche "Arbeit" und wie werden die (bzw. sollen) die Fehlerinfos angeboten?

Würden wir die Nachrichten erst in die DB schreiben und irgendwann später versenden

Warum bringst du eine DB ins Spiel?

Das Zeitfenster von zwei Monaten kommt daher, dass es Prozesse gibt, an die sich die Anwender halten müssen. Ab Datum X wird ein Prozess freigegeben und für zwei Monate können die Anwender dann an diesem Prozess arbeiten (z.B. einen Antrag stellen), danach wird's dann wieder zu gemacht.

Wie ist hier der Zusammenhang mit den Requests?
Es wird wohl kein async Task max. 2 Monate brauchen 😉

Für dieses eine Projekt eine async->sync-Krücke, die ich dann später wieder abstoßen kann - solange es kein Deadlock-Risiko gibt

Mit "sync over async" und max. 8000 Requests wird es so od. so Probleme geben, v.a. in Hinsicht "sofort" (wie immer das auch definiert sein mag).

Dass die generelle Performance bei vielen gleichzeitigen Requests schlecht sein könnte

Was passiert denn nun wenn ein Request zum IIS kommt?
Wird dann das externe API aufgerufen? Das ist doch die zentrale Frage vom Problem und ob dann jeder Request eigentständig das Gleiche machen muss od. ob das irgendwie zusammengefasst werden kann. Wobei das "Zusammenfassen" hängt halt davon ab was gemacht wird und das behälts du uns vor.

Wenn nun für eine Hack-Lösung ein weiterer Hack eingebaut wird, so wird es wohl auch nicht besser werden.
Was passiert z.B. bei einem Windows-Update wenn dort die Logik vom ThreadPool (jenem von Windows) geändert wird unter der Annahme dass der IIS solche Threads verwendet? Was passiert wenn der IIS ein Update erfährt? Wer soll dann wissen wie und weshalb es zu Problemen kommen kann wenn 4000, 5000 od. gar die 8000 Requests kommen? Od. falls es in Zukunft vllt. doch eher 10.000 Requests werden könnten (zu Spitzenzeiten, etc.) und alles so langsam wird dass "sofort" nicht mehr gilt.

Mit den spärlichen Infos ist es mir schier unmöglich konstruktiv beizutragen. Wie vorhin schon gefragt, beschreib doch die wichtigen Punkte und nicht warum was nicht geändert werden kann, etc.

mfG Gü

Stellt fachliche Fragen bitte im Forum, damit von den Antworten alle profitieren. Daher beantworte ich solche Fragen nicht per PM.

"Alle sagten, das geht nicht! Dann kam einer, der wusste das nicht - und hat's gemacht!"

Wann kommen dabei die eingangs genannten Requests (via IIS) ins Spiel?

Es ist eine Web-Anwendung, der Nutzer tut irgendwas und als Folge davon werden Benachrichtigungen (nicht zwingend an den selben Benutzer) versendet.

Hat jeder Benutzer hier seine eigenen Daten od. sind die Daten für viele / alle Benutzer die gleichen?

Sowohl als auch - vermutlich 😄 Tatsächlich weiß ich das nicht sicher, das wird erst noch erarbeitet und hängt nicht nur von uns ab.

Es sollte schon auch vom Worst-Case ausgegangen werden. Oder zumindest von einer 90% Häufigkeit

Daher die 8000 gleichzeitiger Nutzer, das wäre der geschätzte Worst-Case.
Allerdings kostet das bisher schon (zu) viel Zeit, es steht also auch die Frage im Raum, ob es nicht besser wäre, diesen Worst-Case einfach zu akzeptieren, solange es in den meisten Fällen vernünftig läuft und die Anwendung sich nicht komplett aufhängt.

Was heißt "sofort" (siehe vorhin)?
Wohin wird die Antwort geschickt?

Ggf. haben wir hier ein Kommunikationsproblem?

Mit "sofort" meine ich aus Code-Sicht "sofort", also blöd gesagt: In der nächsten Code-Zeile brauche ich das Ergebnis.
Entweder ich warte im Code synchron oder asynchron (mit await), hauptsache ich kann direkt in der nächsten Zeile im selben Request mit der selben Transaktion darauf reagieren. Ich kann die API-Calls aber nicht im Hintergrund ausführen lassen und nach Beendigung den Benutzer informieren, auch wenn es sich für ihn ggf. wie "sofort" anfühlt.

Die Antwort (z.B. eine Id) wird dann in die DB gespeichert, um irgendwann später (Hintergrund-Job) den Status abfragen und darauf reagieren zu können.
Wenn beim Versenden etwas schief ging, dann muss darauf reagiert werden, indem die komplette Transaktion zurückgerollt wird und dem Benutzer ein Fehler angezeigt wird.
Das Status-Abfragen ist dagegen leichter, weil es (vermutlich) nur im Hintergrund-Thread läuft, da können wir dann hoffentlich asynchron arbeiten.

Welche "Arbeit" und wie werden die (bzw. sollen) die Fehlerinfos angeboten?

Was auch immer der Request tun sollte, das kann alles mögliche sein, effektiv alles, was die Anwendung so tut.
Und angeboten werden die Fehlerinfos nur generisch, weil das dann sehr wahrscheinlich ein Programmierfehler oder schlechte Absprache mit Dritten war, also nichts, wofür der Anwender etwas kann bzw. was er beheben kann. Aber das steht auch noch nicht 100% fest, wir bekommen dazu erst noch Infos.

Warum bringst du eine DB ins Spiel?

Das war eine Überlegung, wie wir das Problem umgehen können.
Wenn wir die Nachrichten nicht versenden, sondern erst speichern und dann in einem Hintergrund-Thread asynchron versenden, gäbe es das Problem nicht mehr.
Das funktioniert aber nicht, da der Request, der die Nachricht versendet, nach dem Versenden selber mit dem Ergebnis weiter arbeiten können muss.

Wie ist hier der Zusammenhang mit den Requests?
Es wird wohl kein async Task max. 2 Monate brauchen

"Prozess" ist hier aus Anwendersicht gemeint.
Also wenn Du online z.B. irgendetwas beantrags, dann füllst Du Formulare aus, bestätigst Kram, wirst informiert, etc.
Das alles ist eine Web-Anwendung, das alles passiert also über Web-Requests.
Und der Zeitraum, in dem Du so einen Antrag einreichen darfst, ist fix vorgegeben und dein Zeitfenster sind 2 Monate.

Mit "sync over async" und max. 8000 Requests wird es so od. so Probleme geben, v.a. in Hinsicht "sofort"

Das ist mir/uns bewusst. Im Worst-Case ist's langsam, daran können wir im Moment nichts ändern, hauptsache die Anwendung läuft weiter und "fängt" sich irgendwann wieder, wenn weniger Leute online sind.
Das ist auch ein generelles Thema, da habe ich gar keinen Einfluss drauf.

Was passiert denn nun wenn ein Request zum IIS kommt?
Wird dann das externe API aufgerufen?

Es passiert alles mögliche. Im Request stehen Daten, es werden Daten in der DB aktualisiert, der Code stellt fest, dass Person X eine Nachricht braucht und ruft die API auf. Es kann aber auch ein Hintergrund-Thread sein, der nachts los rennt und Anträge (um beim Beispiel zu bleiben) aktualisiert und wenn irgendetwas aktualisiert wurde, werden Personen informiert, das läuft wieder über die API.

Wenn nun für eine Hack-Lösung ein weiterer Hack eingebaut wird, so wird es wohl auch nicht besser werden.

Natürlich wird das nicht besser, das ist klar.
Aber das Projekt soll eh neu entwickelt werden - dann natürlich alles asynchron und mit Erfahrungen aus den Fehlern 
Deshalb hält sich auch der Wille in Grenzen, solche grundlegenden Probleme anzugehen. Wäre es wenig Aufwand, ok, aber das wäre sehr viel Aufwand, also lieber dafür sorgen, dass es keinen Crash und keinen Deadlock gibt und der Rest ist dann halt so.

Und auch die vielen möglichen Fallstricke, wenn sich das Verhalten vom ThreadPool oder dem IIS ändert:
Ja, das ist ein Problem, aber keines, was wir sinnvoll lösen können.
Ich kann das mit in die Überlegungen einbeziehen, ob es besser ist, die Krücke zu bauen, oder ob ich lieber den ganzen API-Client einmal für HttpClient und einmal für WebClient anbiete. Aber da das Projekt eh nicht mehr super lange leben wird, tendiere ich eher zur Krücke, als jetzt noch mehr Zeit da rein zu stecken.

Mit den spärlichen Infos ist es mir schier unmöglich konstruktiv beizutragen. Wie vorhin schon gefragt, beschreib doch die wichtigen Punkte und nicht warum was nicht geändert werden kann

Im Grunde hast Du alles, was ich weiß 
Ich glaube viel eher, wir hängen an einem Kommunikationsproblem fest?

NuGet Packages im Code auslesen
lock Alternative für async/await

Beim CleanCode zählen nicht die Regeln, sondern dass wir uns mit diesen Regeln befassen, selbst wenn wir sie nicht befolgen - hoffentlich nach reiflichen Überlegungen.