库中的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)
(由于ConfigureAwait
是Task
和Task.Yield
上的方法返回自定义等待,因此不存在)只是使用Task.Factory.StartNew
和CancellationToken.None
, TaskCreationOptions.PreferFairness
和TaskScheduler.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.Wait
, Task.WaitAll
, Task.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在回答同一个问题时提供了略微不同的实现 。 他的IgnoreSynchronizationContext
在IgnoreSynchronizationContext
恢复原始同步上下文(可能是一个完全不同的随机池线程)。 只要我不关心它,我宁愿在await
之后再恢复它。
由于缺少您正在寻找的有用且合法的API,我提交了此请求,建议将其添加到.NET中。
我还将它添加到vs-threading,以便下一版本的Microsoft.VisualStudio.Threading NuGet包将包含此API。 请注意,此库不是特定于VS的,因此您可以在应用中使用它。