库中的Task.Yield()需要ConfigureWait(false)

建议您随时使用ConfigureAwait(false) ,尤其是在库中,因为它可以帮助避免死锁并提高性能。

我编写了一个大量使用async的库(访问数据库的Web服务)。 该库的用户遇到了僵局,经过多次痛苦的调试和修补后,我将其追踪到单独使用await Task.Yield() 。 我等待的其他地方,我使用.ConfigureAwait(false) ,但是在Task.Yield()上不支持。

对于需要相当于Task.Yield().ConfigureAwait(false)情况,建议使用什么解决方案Task.Yield().ConfigureAwait(false)

我已经了解了如何删除SwitchTo方法 。 我可以看出为什么这可能是危险的,但为什么没有相当于Task.Yield().ConfigureAwait(false)

编辑:

为了提供我的问题的进一步背景,这里是一些代码。 我正在实现一个开源库,用于访问支持异步的DynamoDB(作为AWS的服务的分布式数据库)。 许多操作返回IX-Async库提供的IAsyncEnumerable 。 该库不提供从以“块”提供行的数据源生成异步枚举的好方法,即每个异步请求返回许多项。 所以我有自己的通用类型。 该库支持预读选项,允许用户指定在调用MoveNext()实际需要之前应该请求多少数据。

基本上,这是如何工作的,我通过调用GetMore()并在这些之间传递状态来请求块。 我将这些任务放在一个chunks队列中并将它们出列并将它们转换为我放入单独队列的实际结果。 NextChunk()方法是这里的问题。 根据ReadAhead的值,我将在最后一个完成(All)之后保持获取下一个块,直到需要一个值但不可用(None)或仅获取超出当前值的下一个块使用(一些)。 因此,获取下一个块应该并行/不阻止获取下一个值。 枚举器代码是:

 private class ChunkedAsyncEnumerator : IAsyncEnumerator { private readonly ChunkedAsyncEnumerable enumerable; private readonly ConcurrentQueue<Task> chunks = new ConcurrentQueue<Task>(); private readonly Queue results = new Queue(); private CancellationTokenSource cts = new CancellationTokenSource(); private TState lastState; private TResult current; private bool complete; // whether we have reached the end public ChunkedAsyncEnumerator(ChunkedAsyncEnumerable enumerable, TState initialState) { this.enumerable = enumerable; lastState = initialState; if(enumerable.ReadAhead != ReadAhead.None) chunks.Enqueue(NextChunk(initialState)); } private async Task NextChunk(TState state, CancellationToken? cancellationToken = null) { await Task.Yield(); // ** causes deadlock var nextState = await enumerable.GetMore(state, cancellationToken ?? cts.Token).ConfigureAwait(false); if(enumerable.ReadAhead == ReadAhead.All && !enumerable.IsComplete(nextState)) chunks.Enqueue(NextChunk(nextState)); // This is a read ahead, so it shouldn't be tied to our token return nextState; } public Task MoveNext(CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); if(results.Count > 0) { current = results.Dequeue(); return TaskConstants.True; } return complete ? TaskConstants.False : MoveNextAsync(cancellationToken); } private async Task MoveNextAsync(CancellationToken cancellationToken) { Task nextStateTask; if(chunks.TryDequeue(out nextStateTask)) lastState = await nextStateTask.WithCancellation(cancellationToken).ConfigureAwait(false); else lastState = await NextChunk(lastState, cancellationToken).ConfigureAwait(false); complete = enumerable.IsComplete(lastState); foreach(var result in enumerable.GetResults(lastState)) results.Enqueue(result); if(!complete && enumerable.ReadAhead == ReadAhead.Some) chunks.Enqueue(NextChunk(lastState)); // This is a read ahead, so it shouldn't be tied to our token return await MoveNext(cancellationToken).ConfigureAwait(false); } public TResult Current { get { return current; } } // Dispose() implementation omitted } 

我没有声称这段代码是完美的。 对不起它太长了,不知道如何简化。 重要的部分是NextChunk方法和对Task.Yield()的调用。 此function通过静态构造方法使用:

 internal static class AsyncEnumerableEx { public static IAsyncEnumerable GenerateChunked( TState initialState, Func<TState, CancellationToken, Task> getMore, Func<TState, IEnumerable> getResults, Func isComplete, ReadAhead readAhead = ReadAhead.None) { ... } } 

完全相当于Task.Yield().ConfigureAwait(false) (由于ConfigureAwaitTaskTask.Yield上的方法返回自定义等待,因此不存在)只是使用Task.Factory.StartNewCancellationToken.NoneTaskCreationOptions.PreferFairnessTaskScheduler.Current但是,在大多数情况下, Task.Run (使用默认的TaskScheduler )足够接近

您可以通过查看YieldAwaiter的源来validation它,并且当TaskScheduler.Current是默认值(即线程池)时,它会使用ThreadPool.QueueUserWorkItem / ThreadPool.UnsafeQueueUserWorkItem ,而当它不是时,可以使用Task.Factory.StartNew

然而,您可以创建自己的等待( 就像我做的那样 )模仿YieldAwaitable但忽略SynchronizationContext

 async Task Run(int input) { await new NoContextYieldAwaitable(); // executed on a ThreadPool thread } public struct NoContextYieldAwaitable { public NoContextYieldAwaiter GetAwaiter() { return new NoContextYieldAwaiter(); } public struct NoContextYieldAwaiter : INotifyCompletion { public bool IsCompleted { get { return false; } } public void OnCompleted(Action continuation) { var scheduler = TaskScheduler.Current; if (scheduler == TaskScheduler.Default) { ThreadPool.QueueUserWorkItem(RunAction, continuation); } else { Task.Factory.StartNew(continuation, CancellationToken.None, TaskCreationOptions.PreferFairness, scheduler); } } public void GetResult() { } private static void RunAction(object state) { ((Action)state)(); } } } 

注意:我不建议实际使用NoContextYieldAwaitable ,它只是你问题的答案。 您应该使用Task.Run (或Task.Factory.StartNew与特定的TaskScheduler

我注意到你在接受现有答案后编辑了你的问题,所以也许你对这个问题的更多咆哮感兴趣。 干得好 :)

建议您随时使用ConfigureAwait(false),尤其是在库中,因为它可以帮助避免死锁并提高性能。

建议这样做,只有当您完全确定您的实现中调用的任何API(包括Framework API)不依赖于同步上下文的任何属性时。 这对于库代码尤为重要,如果库适合客户端和服务器端使用,则更是如此。 例如, CurrentCulture是一个常见的忽视 :它对桌面应用程序永远不会是一个问题,但它可能适用于ASP.NET应用程序。

回到你的代码:

 private async Task NextChunk(...) { await Task.Yield(); // ** causes deadlock var nextState = await enumerable.GetMore(...); // ... return nextState; } 

最有可能的是,死锁是由您的库的客户端引起的,因为他们使用Task.Result (或Task.WaitTask.WaitAllTask.IAsyncResult.AsyncWaitHandle等,让他们搜索)在调用链的外框中的某个地方。 虽然Task.Yield()在这里是多余的,但这首先不是你的问题,而是他们的问题:它们不应该在异步API上阻塞,而应该使用“Async All the Way” ,如同在斯蒂芬克莱里的文章你链接。

删除Task.Yield() 可能会可能不会解决此问题,因为enumerable.GetMore()也可以使用一些await SomeApiAsync()而不使用ConfigureAwait(false) ,从而将延续发布回调用者的同步上下文。 此外,“ SomeApiAsync ”可能恰好是一个完善的Framework API,它仍然容易受到死锁的影响,比如SendMailAsync ,我们稍后会再回过头来看。

总的来说,你应该只使用Task.Yield()如果由于某种原因你想立即返回调用者(“将执行控制”“返回给调用者”),然后异步继续,由安装的SynchronizationContext支配调用线程(或ThreadPool ,如果SynchronizationContext.Current == null )。 在app的核心消息循环的下一次迭代时,可以在同一线程上执行continuation well。 更多细节可以在这里找到:

  • Task.Yield – 真实用法?

所以,正确的做法是避免一直阻塞代码。 但是,您仍然希望使代码具有死锁function,您不关心同步上下文,并且您确定在实现中使用的任何系统或第三方API都是如此。

然后,而不是重新发明ThreadPoolEx.SwitchTo (由于一个很好的理由被删除),你可以使用Task.Run ,如评论中所建议的:

 private Task NextChunk(...) { // jump to a pool thread without SC to avoid deadlocks return Task.Run(async() => { var nextState = await enumerable.GetMore(...); // ... return nextState; }); } 

IMO, 这仍然是一个hack ,具有相同的净效果,虽然比使用ThreadPoolEx.SwitchTo()的变体更易读。 与SwitchTo相同,它仍然有相关的成本:冗余线程切换可能会损害ASP.NET性能。

还有另一个(IMO更好)hack ,我在这里提出用前面提到的SendMailAsync来解决死锁问题。 它不会产生额外的线程切换:

 private Task NextChunk(...) { return TaskExt.WithNoContext(async() => { var nextState = await enumerable.GetMore(...); // ... return nextState; }); } public static class TaskExt { public static Task WithNoContext(Func> func) { Task task; var sc = SynchronizationContext.Current; try { SynchronizationContext.SetSynchronizationContext(null); task = func(); // do not await here } finally { SynchronizationContext.SetSynchronizationContext(sc); } return task; } } 

此hack的工作方式是暂时删除原始NextChunk方法的同步作用域的同步上下文,因此不会捕获async lambda内的第一个await continuation,从而有效地解决了死锁问题。

Stephen在回答同一个问题时提供了略微不同的实现 。 他的IgnoreSynchronizationContextIgnoreSynchronizationContext恢复原始同步上下文(可能是一个完全不同的随机池线程)。 只要我不关心它,我宁愿在await之后再恢复它。

由于缺少您正在寻找的有用且合法的API,我提交了此请求,建议将其添加到.NET中。

我还将它添加到vs-threading,以便下一版本的Microsoft.VisualStudio.Threading NuGet包将包含此API。 请注意,此库不是特定于VS的,因此您可以在应用中使用它。