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

时间:2015-02-03 21:33:20

标签: c# .net task-parallel-library async-await

它的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)
    { ... }
}

3 个答案:

答案 0 :(得分:8)

完全等同于Task.Yield().ConfigureAwait(false)(自ConfigureAwait以来不存在,TaskTask.Yield上的方法返回自定义等待)只是使用{ {1}} Task.Factory.StartNewCancellationToken.NoneTaskCreationOptions.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)并不依赖于同步上下文的任何属性时。这对于库代码尤为重要,如果库适合客户端和服务器端使用,则更是如此。例如,CurrentCulturecommon overlook:它对于桌面应用程序永远不会成为问题,但它可能适用于ASP.NET应用程序。

返回您的代码:

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

最有可能的是,死锁是由您的图书馆的客户造成的,因为他们使用Task.Result(或Task.WaitTask.WaitAllTask.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的,因此您可以在应用中使用它。