[英]Task.Yield() in library needs ConfigureWait(false)
建议您随时使用ConfigureAwait(false)
,尤其是在库中,因为它可以帮助避免死锁并提高性能。
我编写了一个大量使用异步的库(访问数据库的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<T>
。 该库不提供从以“块”提供行的数据源生成异步枚举的好方法,即每个异步请求返回许多项。 所以我有自己的通用类型。 该库支持预读选项,允许用户指定在调用MoveNext()
实际需要之前应该请求多少数据。
基本上,这是如何工作的,我通过调用GetMore()
并在这些之间传递状态来请求块。 我将这些任务放在一个chunks
队列中并将它们出列并将它们转换为我放在一个单独队列中的实际结果。 NextChunk()
方法是这里的问题。 根据ReadAhead
的值,我将在最后一个完成(All)之后保持获取下一个块,直到需要一个值但不可用(None)或仅获得超出当前值的下一个块使用(一些)。 因此,获取下一个块应该并行/不阻止获取下一个值。 枚举器代码是:
private class ChunkedAsyncEnumerator<TState, TResult> : IAsyncEnumerator<TResult>
{
private readonly ChunkedAsyncEnumerable<TState, TResult> enumerable;
private readonly ConcurrentQueue<Task<TState>> chunks = new ConcurrentQueue<Task<TState>>();
private readonly Queue<TResult> results = new Queue<TResult>();
private CancellationTokenSource cts = new CancellationTokenSource();
private TState lastState;
private TResult current;
private bool complete; // whether we have reached the end
public ChunkedAsyncEnumerator(ChunkedAsyncEnumerable<TState, TResult> enumerable, TState initialState)
{
this.enumerable = enumerable;
lastState = initialState;
if(enumerable.ReadAhead != ReadAhead.None)
chunks.Enqueue(NextChunk(initialState));
}
private async Task<TState> 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<bool> MoveNext(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
if(results.Count > 0)
{
current = results.Dequeue();
return TaskConstants.True;
}
return complete ? TaskConstants.False : MoveNextAsync(cancellationToken);
}
private async Task<bool> MoveNextAsync(CancellationToken cancellationToken)
{
Task<TState> 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()
的调用。 此功能通过静态构造方法使用:
internal static class AsyncEnumerableEx
{
public static IAsyncEnumerable<TResult> GenerateChunked<TState, TResult>(
TState initialState,
Func<TState, CancellationToken, Task<TState>> getMore,
Func<TState, IEnumerable<TResult>> getResults,
Func<TState, bool> 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
的源来验证它,并且当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<TState> 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” ,如同在史蒂芬Cleary的文章你链接。
删除Task.Yield()
可能会也可能不会解决此问题,因为enumerable.GetMore()
也可以使用一些await SomeApiAsync()
而不使用ConfigureAwait(false)
,从而将延续发送回调用者的同步上下文。 此外,“ SomeApiAsync
”可能恰好是一个完善的Framework API,它仍然容易受到死锁的影响,比如SendMailAsync
,我们稍后会再回过头来看。
总的来说,你应该只使用Task.Yield()
如果由于某种原因你想立即返回调用者(“将执行控制”“返回给调用者”),然后异步继续,由安装的SynchronizationContext
支配。调用线程(或ThreadPool
,如果SynchronizationContext.Current == null
)。 在app的核心消息循环的下一次迭代时,可以在同一线程上执行continuation well。 更多细节可以在这里找到:
所以,正确的做法是避免一直阻塞代码。 但是,您仍然希望使代码具有死锁功能,您不关心同步上下文,并且您确定在实现中使用的任何系统或第三方API都是如此。
然后,而不是重新发明ThreadPoolEx.SwitchTo
(由于一个很好的理由被删除),你可以使用Task.Run
,如评论中所建议的:
private Task<TState> 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<TState> NextChunk(...)
{
return TaskExt.WithNoContext(async() =>
{
var nextState = await enumerable.GetMore(...);
// ...
return nextState;
});
}
public static class TaskExt
{
public static Task<TResult> WithNoContext<TResult>(Func<Task<TResult>> func)
{
Task<TResult> 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
延续,从而有效地解决了死锁问题。
Stephen在回答相同问题时提供了略微不同的实现 。 他IgnoreSynchronizationContext
恢复上无论发生什么事是以后继续的线程原来的同步上下文await
(这可能是一个完全不同的,随机池线程)。 只要我不关心它,我宁愿在await
之后再恢复它。
由于缺少您正在寻找的有用且合法的API,我提交了此请求,建议将其添加到.NET中。
我还将它添加到vs-threading,以便下一版本的Microsoft.VisualStudio.Threading NuGet包将包含此API。 请注意,此库不是特定于VS的,因此您可以在应用中使用它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.