它的recommended that one use ConfigureAwait(false)
whenever when you can,特别是在库中,因为它可以帮助避免死锁并提高性能。
我编写了一个大量使用异步的库(访问数据库的Web服务)。该库的用户遇到了僵局,经过大量痛苦的调试和修补后,我将其追踪到await Task.Yield()
的单次使用。在我等待的其他地方,我使用.ConfigureAwait(false)
,但Task.Yield()
不支持。
对于需要等同于Task.Yield().ConfigureAwait(false)
?
我已经了解了SwitchTo
method that was removed的情况。我可以看出为什么这可能是危险的,但为什么没有相当于Task.Yield().ConfigureAwait(false)
?
修改
为了提供我的问题的进一步背景,这里有一些代码。我正在实现一个open source library来访问支持异步的DynamoDB(作为AWS的服务的分布式数据库)。许多操作返回IX-Async library提供的IAsyncEnumerable<T>
。该库没有提供一种从数据源生成异步枚举的好方法,这些数据源在&#34;块&#34;中提供行。即每个异步请求返回许多项。所以我有自己的通用类型。该库支持预读选项,允许用户在调用MoveNext()
实际需要之前指定应该请求的数据量。
基本上,这是如何工作的,我通过调用GetMore()
并在这些之间传递状态来请求块。我将这些任务放在chunks
队列中并将它们出列并将它们转换为我放在单独队列中的实际结果。 NextChunk()
方法就是问题所在。取决于ReadAhead
的值,我将在最后一个完成(全部)之后保持获取下一个块,直到需要一个值但不可用(无)或仅获取超出值的下一个块目前正在使用(一些)。因此,获取下一个块应该并行/不阻止获取下一个值。枚举器代码是:
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)
{ ... }
}
答案 0 :(得分:8)
完全等同于Task.Yield().ConfigureAwait(false)
(自ConfigureAwait
以来不存在,Task
和Task.Yield
上的方法返回自定义等待)只是使用{ {1}} Task.Factory.StartNew
,CancellationToken.None
和TaskCreationOptions.PreferFairness
。 但在大多数情况下,TaskScheduler.Current
(使用默认Task.Run
)足够接近。
您可以查看YieldAwaiter
的来源,并在TaskScheduler
是默认值(即线程池)时看到它使用ThreadPool.QueueUserWorkItem
/ ThreadPool.UnsafeQueueUserWorkItem
来验证这一点TaskScheduler.Current
当它不是。
然而,您可以创建自己的等待(as I did)来模仿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
)
答案 1 :(得分:6)
我注意到您在接受现有答案后编辑了您的问题,所以也许您对此主题的更多咆哮感兴趣。在这里你去:))
建议您随时使用ConfigureAwait(false) 可以,特别是在图书馆,因为它可以帮助避免死锁和 提高绩效。
建议您这样做,只有当您完全确定您在实现中调用的任何API(包括Framework API)并不依赖于同步上下文的任何属性时。这对于库代码尤为重要,如果库适合客户端和服务器端使用,则更是如此。例如,CurrentCulture
是common overlook:它对于桌面应用程序永远不会成为问题,但它可能适用于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上阻塞,而应该使用&#34;异步所有方式&#34; ,正如您链接的Stephen Cleary的文章中所述。
删除Task.Yield()
可能或可能无法解决此问题,因为enumerable.GetMore()
也可以使用某些await SomeApiAsync()
而不{{1}因此,将延续发布回调用者的同步上下文。而且,&#34; ConfigureAwait(false)
&#34;可能恰好是一个完善的Framework API,它仍然容易陷入僵局,比如SomeApiAsync
,我们稍后会再回到它。
总的来说,如果由于某种原因你想立即返回调用者(&#34; yield&#34;执行控制回到调用者),你应该只使用SendMailAsync
,然后异步地继续,由Task.Yield()
安装在调用线程上(或SynchronizationContext
,如果ThreadPool
)。在app的核心消息循环的下一次迭代时,可以在相同的线程上执行继续井。更多细节可以在这里找到:
所以,正确的做法是避免一直阻塞代码。但是,您仍然希望使代码具有死锁功能,您不必关心同步上下文,并且您确定在实现中使用的任何系统或第三方API都是如此。< / p>
然后,您可以按照评论中的建议使用SynchronizationContext.Current == null
,而不是重新发明ThreadPoolEx.SwitchTo
(有充分理由将其删除):
Task.Run
IMO,这仍然是一个黑客,具有相同的净效果,虽然比使用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;
});
}
的变体更具可读性。与ThreadPoolEx.SwitchTo()
相同,它仍然有相关的成本:冗余线程切换可能会损害ASP.NET性能。
另一个(IMO更好)黑客,我建议here使用前面提到的SwitchTo
来解决僵局。它不会产生额外的线程切换:
SendMailAsync
此hack的工作方式是暂时删除原始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;
}
}
方法的同步范围的同步上下文,因此不会为NextChunk
内的await
延续捕获{1}} lambda,有效地解决了死锁问题。
Stephen在回答different implementation时提供了一点点same question。他的async
恢复原始同步上下文,无论是IgnoreSynchronizationContext
之后的延续线程(可能是一个完全不同的随机池线程)。我完全不会在await
之后恢复它,只要我不关心它。
答案 2 :(得分:1)
由于缺少您正在寻找的有用且合法的API,我提交this request建议将其添加到.NET中。
我还added it to vs-threading,以便Microsoft.VisualStudio.Threading NuGet package的下一个版本将包含此API。请注意,此库不是特定于VS的,因此您可以在应用中使用它。