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.Post
Im 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.