AspNetSynchronizationContext 并等待 ASP.NET 中的延续 [英] AspNetSynchronizationContext and await continuations in ASP.NET
问题描述
在异步 ASP.NET Web API 控制器方法中的 await
之后,我注意到一个意外的(我会说是冗余的)线程切换.
I noticed an unexpected (and I'd say, a redundant) thread switch after await
inside asynchronous ASP.NET Web API controller method.
例如,下面我希望在位置 #2 和 3# 处看到相同的 ManagedThreadId
,但大多数情况下我在 #3 处看到不同的线程:
For example, below I'd expect to see the same ManagedThreadId
at locations #2 and 3#, but most often I see a different thread at #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";
}
}
我查看了 AspNetSynchronizationContext.Post
,本质上归结为:
I've looked at the implementation of AspNetSynchronizationContext.Post
, essentially it comes down to this:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
因此,在 ThreadPool
上安排延续,而不是内联. 这里,ContinueWith
使用 TaskScheduler.Current
,根据我的经验,它始终是 ASP.NET 中 ThreadPoolTaskScheduler
的一个实例(但不一定是这样,见下文).
Thus, the continuation is scheduled on ThreadPool
, rather than gets inlined. Here, ContinueWith
uses TaskScheduler.Current
, which in my experience is always an instance of ThreadPoolTaskScheduler
inside ASP.NET (but it doesn't have to be that, see below).
我可以使用 ConfigureAwait(false)
或自定义等待器消除像这样的冗余线程切换,但这会取消 HTTP 请求状态属性的自动流动,例如 HttpContext.Current代码>.
I could eliminate a redundant thread switch like this with ConfigureAwait(false)
or a custom awaiter, but that would take away the automatic flow of the HTTP request's state properties like HttpContext.Current
.
AspNetSynchronizationContext.Post
的当前实现还有另一个副作用.在以下情况下会导致死锁:
There's another side effect of the current implementation of AspNetSynchronizationContext.Post
. It results in a deadlock in the following case:
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();
这个例子,虽然有点做作,但显示了如果 TaskScheduler.Current
是 TaskScheduler.FromCurrentSynchronizationContext()
,即,由 AspNetSynchronizationContext
制成,可能会发生什么代码>.它不使用任何阻塞代码,并且在 WinForms 或 WPF 中可以顺利执行.
This example, albeit a bit contrived, shows what may happen if TaskScheduler.Current
is TaskScheduler.FromCurrentSynchronizationContext()
, i.e., made from AspNetSynchronizationContext
. It doesn't use any blocking code and would have been executed smoothly in WinForms or WPF.
AspNetSynchronizationContext
的这种行为不同于 v4.0 实现(它仍然作为 LegacyAspNetSynchronizationContext
).
This behavior of AspNetSynchronizationContext
is different from the v4.0 implementation (which is still there as LegacyAspNetSynchronizationContext
).
那么,这种变化的原因是什么?我想,这背后的想法可能是为了减少死锁的差距,但是当使用 Task.Wait()
或 Task.Result
.
So, what is the reason for such change? I thought, the idea behind this might be to reduce the gap for deadlocks, but deadlock are still possible with the current implementation, when using Task.Wait()
or Task.Result
.
IMO,这样说更合适:
IMO, it'd more appropriate to put it like this:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;
或者,至少,我希望它使用 TaskScheduler.Default
而不是 TaskScheduler.Current
.
Or, at least, I'd expect it to use TaskScheduler.Default
rather than TaskScheduler.Current
.
如果我在 web.config
中使用 <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false"/>
启用 LegacyAspNetSynchronizationContext
>,它按预期工作:同步上下文安装在等待任务结束的线程上,并在那里同步执行继续.
If I enable LegacyAspNetSynchronizationContext
with <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
in web.config
, it works as desired: the synchronization context gets installed on the thread where the awaited task has ended, and the continuation is synchronously executed there.
推荐答案
现在我的猜测是,他们已经通过这种方式实现了 AspNetSynchronizationContext.Post
以避免可能导致堆栈溢出的无限递归的可能性.如果从传递给 Post
本身的回调中调用 Post
,则可能会发生这种情况.
Now my guess is, they have implemented AspNetSynchronizationContext.Post
this way to avoid a possibility of infinite recursion which might lead to stack overflow. That might happen if Post
is called from the callback passed to Post
itself.
不过,我认为额外的线程切换可能太贵了.本来可以这样避免的:
Still, I think an extra thread switch might be too expensive for this. It could have been possibly avoided like this:
var sameStackFrame = true
try
{
//TODO: also use TaskScheduler.Default rather than TaskScheduler.Current
Task newTask = _lastScheduledTask.ContinueWith(completedTask =>
{
if (sameStackFrame) // avoid potential recursion
return completedTask.ContinueWith(_ => SafeWrapCallback(action));
else
{
SafeWrapCallback(action);
return completedTask;
}
}, TaskContinuationOptions.ExecuteSynchronously).Unwrap();
_lastScheduledTask = newTask;
}
finally
{
sameStackFrame = false;
}
基于这个想法,我创建了一个自定义等待器,它为我提供了所需的行为:
Based on this idea, I've created a custom awaiter which gives me the desired behavior:
await task.ConfigureContinue(synchronously: true);
如果操作在同一堆栈帧上同步完成,则使用 SynchronizationContext.Post
,如果在不同的堆栈帧上完成,则使用 SynchronizationContext.Send
(它甚至可以是相同的线程,在一些循环后被 ThreadPool
异步重用):
It uses SynchronizationContext.Post
if operation completed synchronously on the same stack frame, and SynchronizationContext.Send
if it did on a different stack frame (it could even be the same thread, asynchronously reused by ThreadPool
after some cycles):
using System;
using System.Diagnostics;
using System.Runtime.Remoting.Messaging;
using System.Threading;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
namespace TestApp.Controllers
{
/// <summary>
/// TestController
/// </summary>
public class TestController : ApiController
{
public async Task<string> GetData()
{
Debug.WriteLine(String.Empty);
Debug.WriteLine(new
{
where = "before await",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
// add some state to flow
HttpContext.Current.Items.Add("_context_key", "_contextValue");
CallContext.LogicalSetData("_key", "_value");
var task = Task.Delay(100).ContinueWith(t =>
{
Debug.WriteLine(new
{
where = "inside ContinueWith",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
// return something as we only have the generic awaiter so far
return Type.Missing;
}, TaskContinuationOptions.ExecuteSynchronously);
await task.ConfigureContinue(synchronously: true);
Debug.WriteLine(new
{
logicalData = CallContext.LogicalGetData("_key"),
contextData = HttpContext.Current.Items["_context_key"],
where = "after await",
thread = Thread.CurrentThread.ManagedThreadId,
context = SynchronizationContext.Current
});
return "OK";
}
}
/// <summary>
/// TaskExt
/// </summary>
public static class TaskExt
{
/// <summary>
/// ConfigureContinue - http://stackoverflow.com/q/23062154/1768303
/// </summary>
public static ContextAwaiter<TResult> ConfigureContinue<TResult>(this Task<TResult> @this, bool synchronously = true)
{
return new ContextAwaiter<TResult>(@this, synchronously);
}
/// <summary>
/// ContextAwaiter
/// TODO: non-generic version
/// </summary>
public class ContextAwaiter<TResult> :
System.Runtime.CompilerServices.ICriticalNotifyCompletion
{
readonly bool _synchronously;
readonly Task<TResult> _task;
public ContextAwaiter(Task<TResult> task, bool synchronously)
{
_task = task;
_synchronously = synchronously;
}
// awaiter methods
public ContextAwaiter<TResult> GetAwaiter()
{
return this;
}
public bool IsCompleted
{
get { return _task.IsCompleted; }
}
public TResult GetResult()
{
return _task.Result;
}
// ICriticalNotifyCompletion
public void OnCompleted(Action continuation)
{
UnsafeOnCompleted(continuation);
}
// Why UnsafeOnCompleted? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
public void UnsafeOnCompleted(Action continuation)
{
var syncContext = SynchronizationContext.Current;
var sameStackFrame = true;
try
{
_task.ContinueWith(_ =>
{
if (null != syncContext)
{
// async if the same stack frame
if (sameStackFrame)
syncContext.Post(__ => continuation(), null);
else
syncContext.Send(__ => continuation(), null);
}
else
{
continuation();
}
}, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default);
}
finally
{
sameStackFrame = false;
}
}
}
}
}
这篇关于AspNetSynchronizationContext 并等待 ASP.NET 中的延续的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!