AspNetSynchronizationContext und warten auf Fortsetzungen in ASP.NET

Ich bemerkte einen unerwarteten (und ich würde sagen, redundanten) Threadwechsel danachawait innerhalb der asynchronen ASP.NET-Web-API-Controller-Methode.

Zum Beispiel würde ich unten erwarten, dasselbe zu sehenManagedThreadId an den Positionen 2 und 3, aber am häufigsten sehe ich einen anderen Thread an # 3:

public class TestController : ApiController
{
    public async Task<string> GetData()
    {
        Debug.WriteLine(new
        {
            where = "1) before await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        await Task.Delay(100).ContinueWith(t =>
        {
            Debug.WriteLine(new
            {
                where = "2) inside ContinueWith",
                thread = Thread.CurrentThread.ManagedThreadId,
                context = SynchronizationContext.Current
            });
        }, TaskContinuationOptions.ExecuteSynchronously); //.ConfigureAwait(false);

        Debug.WriteLine(new
        {
            where = "3) after await",
            thread = Thread.CurrentThread.ManagedThreadId,
            context = SynchronizationContext.Current
        });

        return "OK";
    }
}

Ich habe mir die Umsetzung von angeschautAspNetSynchronizationContext.PostIm Wesentlichen kommt es darauf an:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;

Somit,Die Fortsetzung ist am geplantThreadPool, anstatt inliniert zu werden. Hier,ContinueWith VerwendetTaskScheduler.Current, was meiner Erfahrung nach immer ein Beispiel fürThreadPoolTaskScheduler in ASP.NET (muss aber nicht sein, siehe unten).

Ich könnte einen redundanten Thread-Schalter wie diesen mit beseitigenConfigureAwait(false) oder einen benutzerdefinierten Kellner, aber das würde den automatischen Fluss der Statuseigenschaften der HTTP-Anforderung beeinträchtigenHttpContext.Current.

Es gibt einen weiteren Nebeneffekt der aktuellen Implementierung vonAspNetSynchronizationContext.Post. Im folgenden Fall kommt es zu einem Deadlock:

await Task.Factory.StartNew(
    async () =>
    {
        return await Task.Factory.StartNew(
            () => Type.Missing,
            CancellationToken.None,
            TaskCreationOptions.None,
            scheduler: TaskScheduler.FromCurrentSynchronizationContext());
    },
    CancellationToken.None,
    TaskCreationOptions.None,
    scheduler: TaskScheduler.FromCurrentSynchronizationContext()).Unwrap();

Dieses Beispiel zeigt, wenn auch etwas ausgeklügelt, was passieren kann, wennTaskScheduler.Current istTaskScheduler.FromCurrentSynchronizationContext()hergestellt ausAspNetSynchronizationContext. Es verwendet keinen blockierenden Code und wäre in WinForms oder WPF reibungslos ausgeführt worden.

Dieses Verhalten vonAspNetSynchronizationContext unterscheidet sich von der v4.0 - Implementierung (die noch alsLegacyAspNetSynchronizationContext).

Was ist der Grund für eine solche Änderung? Ich dachte, die Idee dahinter könnte sein, die Lücke für Deadlocks zu verkleinern, aber Deadlocks sind mit der aktuellen Implementierung bei der Verwendung immer noch möglichTask.Wait() oderTask.Result.

IMO, es wäre angemessener, es so auszudrücken:

Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
    TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;

Zumindest würde ich damit rechnenTaskScheduler.Default eher, alsTaskScheduler.Current.

Wenn ich es aktiviereLegacyAspNetSynchronizationContext mit<add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" /> imweb.config, es funktioniert wie gewünscht: Der Synchronisationskontext wird auf dem Thread installiert, auf dem die erwartete Aufgabe beendet wurde, und die Fortsetzung wird dort synchron ausgeführt.

Antworten auf die Frage(2)

Ihre Antwort auf die Frage