AspNetSynchronizationContext 并等待 ASP.NET 中的延续
本文关键字:延续 NET ASP 等待 AspNetSynchronizationContext | 更新日期: 2023-09-27 17:58:18
我注意到在异步 ASP.NET Web API 控制器方法中await
后出现了意外的(我会说是冗余的(线程切换。
例如,下面我希望在位置 #2 和 3# 看到相同的ManagedThreadId
,但大多数情况下我在 #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
的实现,基本上可以归结为:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action));
_lastScheduledTask = newTask;
因此,延续被安排在ThreadPool
,而不是内联。在这里,ContinueWith
使用 TaskScheduler.Current
,根据我的经验,这始终是 ASP.NET 内部ThreadPoolTaskScheduler
的实例(但不一定是这样,见下文(。
我可以使用ConfigureAwait(false)
或自定义等待器消除这样的冗余线程开关,但这会带走 HTTP 请求的状态属性的自动流,例如 HttpContext.Current
.
当前实现AspNetSynchronizationContext.Post
还有另一个副作用。在以下情况下,它会导致死锁:
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中顺利执行。
AspNetSynchronizationContext
的这种行为与 v4.0 实现不同(v4.0 实现仍然存在LegacyAspNetSynchronizationContext
(。
那么,这种变化的原因是什么呢?我想,这背后的想法可能是减少死锁的差距,但是当使用Task.Wait()
或Task.Result
时,当前的实现仍然可以使用死锁。
IMO,这样说更合适:
Task newTask = _lastScheduledTask.ContinueWith(_ => SafeWrapCallback(action),
TaskContinuationOptions.ExecuteSynchronously);
_lastScheduledTask = newTask;
或者,至少,我希望它使用TaskScheduler.Default
而不是TaskScheduler.Current
。
如果我在 web.config
中启用带有 <add key="aspnet:UseTaskFriendlySynchronizationContext" value="false" />
的 LegacyAspNetSynchronizationContext
,它会按预期工作:同步上下文安装在等待任务已结束的线程上,并且继续在那里同步执行。
延续被调度到新线程而不是内联是有意的。 让我们分解一下:
-
您正在调用 Task.Delay(100(。 100 毫秒后,基础任务将转换为已完成状态。 但是这种转换将发生在任意线程池/IOCP线程上;它不会发生在 ASP.NET 同步上下文下的线程上。
-
这。ContinueWith(..., ExecuteSyncally( 将导致 Debug.WriteLine(2( 发生在将 Task.Delay(100( 转换为终端状态的线程上。 继续使用本身将返回一个新任务。
-
您正在等待 [2] 返回的任务。 由于完成任务 [2] 的线程不受 ASP.NET 同步上下文的控制,因此 async/await 机制将调用 SynchronizationContext.Post。 此方法始终约定为异步调度。
async/await 机制确实有一些优化,可以在完成线程上内联执行延续,而不是调用 SynchronizationContext.Post,但仅当完成线程当前在其即将调度到的同步上下文下运行时,该优化才会启动。 上面的示例中不是这种情况,因为 [2] 在任意线程池线程上运行,但它需要调度回 AspNetSynchronizationContext 以运行 [3] 延续。 这也解释了为什么如果使用 .ConfigureAwait(false(:[3] 延续可以在 [2] 中内联,因为它将在默认同步上下文下调度。
对于您的其他问题:Task.Wait(( 和 Task.Result,新的同步上下文并非旨在减少相对于 .NET 4.0 的死锁条件。 (事实上,在新同步上下文中出现死锁比在旧上下文中稍微容易一些。 新的同步上下文旨在实现 .Post(( 与 async/await 机制配合得很好,旧的同步上下文在这样做时惨遭失败。 (旧同步上下文的实现 .Post(( 是阻塞调用线程,直到同步原语可用,然后内联调度回调。
在未知已完成的任务上调用请求线程调用 Task.Wait(( 和 Task.Result仍会导致死锁,就像在 Win Forms 或 WPF 应用程序中从 UI 线程调用 Task.Wait(( 或 Task.Result 一样。
最后,Task.Factory.StartNew的怪异可能是一个实际的错误。 但是,除非有一个实际的(非人为的(场景来支持这一点,否则团队不会倾向于进一步调查这一点。
现在我的猜测是,他们已经以这种方式实现了AspNetSynchronizationContext.Post
以避免可能导致堆栈溢出的无限递归的可能性。如果从传递给Post
本身的回调调用Post
,则可能会发生这种情况。
不过,我认为额外的线程开关可能太贵了。它本可以像这样避免:
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;
}
基于这个想法,我创建了一个自定义等待者,它为我提供了所需的行为:
await task.ConfigureContinue(synchronously: true);
如果操作在同一堆栈帧上同步完成,它使用SynchronizationContext.Post
,如果在不同的堆栈帧上完成,则SynchronizationContext.Send
使用(它甚至可以是同一个线程,在几个周期后由ThreadPool
异步重用(:
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;
}
}
}
}
}