【问题标题】:Is there a more readable alternative to calling ConfigureAwait(false) inside an async method?在异步方法中调用 ConfigureAwait(false) 是否有更易读的替代方法?
【发布时间】:2015-01-22 10:16:01
【问题描述】:

我目前正在编写大量 async 库代码,并且我知道在每次异步调用之后添加 ConfigureAwait(false) 以避免将延续代码编组回原始代码(通常是 UI)的做法线程上下文。由于我不喜欢未标记的布尔参数,因此我倾向于将其写为 ConfigureAwait(continueOnCapturedContext: false)

我添加了一个扩展方法以使其更具可读性(并在一定程度上减少打字):

public static class TaskExtensions
{
    public static ConfiguredTaskAwaitable<TResult> WithoutCapturingContext<TResult>(this Task<TResult> task)
    {
        return task.ConfigureAwait(continueOnCapturedContext: false);
    }

    public static ConfiguredTaskAwaitable WithoutCapturingContext(this Task task)
    {
        return task.ConfigureAwait(continueOnCapturedContext: false);
    }
}

所以现在我可以用await SomethingAsync().WithoutCapturingContext() 代替await SomethingAsync().ConfigureAwait(continueOnCapturedContext: false)。我认为这是一种改进,但是当我必须在同一个代码块中调用多个 async 方法时,即使这也开始令人讨厌,因为我最终会得到类似这样的东西:

await FooAsync().WithoutCapturingContext();
var bar = await BarAsync().WithoutCapturingContext();
await MoreFooAsync().WithoutCapturingContext();
var moreBar = await MoreBarAsync().WithoutCapturingContext();
// etc, etc

在我看来,它开始使代码的可读性降低很多。

我的问题基本上是这样的:有没有办法进一步减少这种情况(除了缩短扩展方法的名称)?

【问题讨论】:

  • 你总是给你的所有参数加上标签吗?
  • 您可以将ConfigureAwait() 移至较低级别。例如。在BarAsync() 周围创建一个包装器,基本上是return await BarAsync().WithoutCapturingContext()
  • 你不只需要第一个.WithoutCapturingContext() 吗?第一次调用就足以“摆脱”原始上下文;后续延续是否在相同的非原始上下文中运行并不重要......对吗? (我的意思是,第一次调用可能会回调线程池中方法的其余部分,并且所有进一步的回调也可以在线程池中运行,因此您可以让等待者捕获线程池上下文。)
  • @stakx - 没有。不能保证方法会异步返回(即return Task.FromResult(42); 对于潜在的异步方法来说是完全有效的返回)。所以如果你决定走那条路,你必须在每次通话中指定ConfigureAwait(false)
  • @stakx 那么您将在执行时添加大量要完成的工作,这样您就可以减少作为程序员的打字工作。这意味着您的所有异步方法都需要等待线程池线程能够执行代码,然后才能开始处理任何其他工作,并且您正在添加一个额外的延续结束也是如此。当您开始对几乎所有异步方法以及打算在许多不同上下文(可能对性能敏感)中使用的库代码执行此操作时,就会出现问题。

标签: c# async-await code-formatting


【解决方案1】:

没有全局设置来阻止方法中的任务捕获同步上下文,但您可以做的只是更改该方法范围内的同步上下文。在您的特定情况下,您可以将上下文更改为仅适用于该方法范围的默认同步上下文。

编写一个简单的一次性类来更改同步上下文并在释放时将其更改回来很容易:

public class SyncrhonizationContextChange : IDisposable
{
    private SynchronizationContext previous;
    public SyncrhonizationContextChange(SynchronizationContext newContext = null)
    {
        previous = SynchronizationContext.Current;
        SynchronizationContext.SetSynchronizationContext(newContext);
    }

    public void Dispose()
    {
        SynchronizationContext.SetSynchronizationContext(previous);
    }
}

允许你写:

using(var change = new SyncrhonizationContextChange())
{
    await FooAsync();
    var bar = await BarAsync();
    await MoreFooAsync();
    var moreBar = await MoreBarAsync();
}

(注意将上下文设置为null 意味着它将使用默认上下文。)

【讨论】:

  • 我确实喜欢这个,但是在谷歌搜索了一下之后,它似乎很少使用并且有点不标准。沿着这条路线走会有任何副作用吗?另外,在这种情况下,“默认上下文”是什么意思?
  • 另外,这将如何影响被调用的异步方法?例如,如果从FooAsync 中调用异步方法,它会得到“null”SynchronizationContext,还是我也必须在FooAsync 中复制SynchronizationContextChange
  • @StevenRands 默认上下文正是您使用ConfigureAwait(false) 时使用的上下文。您明确告诉它在计划延续时不要“记住”当前上下文,而是使用默认上下文,它将使用线程池。与其让每个异步方法都不使用当前上下文,不如将当前上下文设置为您要使用的上下文。在设置当前上下文时,它会一直保持这种状态,直到它被设置为其他内容,在这种情况下是当它离开 using 块时。
  • 如果我们不需要整个方法执行的原始上下文,那么恢复它实际上是不必要的,您同意吗?如果调用者的等待者在自己的延续中需要它,肯定已经捕获了它;存储和恢复两次是没有意义的。
  • @tne 是的,如果您不需要该方法范围内的原始上下文,则不需要恢复它。请注意,恢复上下文实际上只是设置静态变量的值。这实际上是您可能不得不做的最简单的操作,因此尝试省略它并不能为您节省很多。添加一个延续以使代码在特定线程中恢复执行与仅设置上下文完全不同。
【解决方案2】:

注意ConfigureAwait(false) 并不意味着忽略同步上下文。有时,它可以push the await continuation to a pool thread,尽管实际延续已在具有非空同步上下文的非池线程上触发。 IMO,ConfigureAwait(false) 的这种行为可能令人惊讶且不直观。至少,它的副作用是冗余线程切换。

如果您真的想在 await 之后忽略延续线程的同步上下文,而只是在该线程/上下文发生的任何情况下同步恢复执行 (TaskContinuationOptions.ExecuteSynchronously),您可以使用自定义等待器:

await MoreFooAsync().IgnoreContext();

以下是IgnoreContext 的可能实现(仅经过少量测试):

public static class TaskExt
{
    // Generic Task<TResult>

    public static IgnoreContextAwaiter<TResult> IgnoreContext<TResult>(this Task<TResult> @task)
    {
        return new IgnoreContextAwaiter<TResult>(@task);
    }

    public struct IgnoreContextAwaiter<TResult> :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task<TResult> _task;

        public IgnoreContextAwaiter(Task<TResult> task)
        {
            _task = task;
        }

        // custom Awaiter methods
        public IgnoreContextAwaiter<TResult> GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public TResult GetResult()
        {
            // result and exceptions
            return _task.GetAwaiter().GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            // not always synchronous, http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx
            _task.ContinueWith(_ => continuation(), TaskContinuationOptions.ExecuteSynchronously);
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            // why SuppressFlow? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
            using (ExecutionContext.SuppressFlow())
            {
                OnCompleted(continuation);
            }
        }
    }

    // Non-generic Task

    public static IgnoreContextAwaiter IgnoreContext(this Task @task)
    {
        return new IgnoreContextAwaiter(@task);
    }

    public struct IgnoreContextAwaiter :
        System.Runtime.CompilerServices.ICriticalNotifyCompletion
    {
        Task _task;

        public IgnoreContextAwaiter(Task task)
        {
            _task = task;
        }

        // custom Awaiter methods
        public IgnoreContextAwaiter GetAwaiter()
        {
            return this;
        }

        public bool IsCompleted
        {
            get { return _task.IsCompleted; }
        }

        public void GetResult()
        {
            // result and exceptions
            _task.GetAwaiter().GetResult();
        }

        // INotifyCompletion
        public void OnCompleted(Action continuation)
        {
            // not always synchronous, http://blogs.msdn.com/b/pfxteam/archive/2012/02/07/10265067.aspx
            _task.ContinueWith(_ => continuation(), TaskContinuationOptions.ExecuteSynchronously);
        }

        // ICriticalNotifyCompletion
        public void UnsafeOnCompleted(Action continuation)
        {
            // why SuppressFlow? http://blogs.msdn.com/b/pfxteam/archive/2012/02/29/10274035.aspx
            using (ExecutionContext.SuppressFlow())
            {
                OnCompleted(continuation);
            }
        }
    }
}

【讨论】:

  • 如果异步方法在 UI 线程中完成,并且重要的是继续在 UI 线程中运行,那么 not 很重要i> 只需同步执行延续。
  • @Servy,如果有这样的要求,那么很容易调整此代码以解决ContinueWith lambda 中的问题。但是,我正在尝试为这些要求提出一个真实的场景,但到目前为止我还无法做到。例如,用 TCS 包装的 WinForm 的计时器或 UI 元素事件?我仍然希望在 UI 线程上同步处理它,而不是将继续推送到非 UI 线程。
【解决方案3】:

简短的回答是否定的。

您必须每次都单独使用ConfigureAwait,没有全局配置或类似的东西。正如您所说,您可以使用名称较短的扩展方法,但这并没有太大变化。您可能可以对您的代码进行某种转换(可能使用 Roslyn),将ConfigureAwait(false) 粘贴到任何地方,但在我看来它是不可靠的。最后,它只是你需要时必须写的东西,比如await;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-06-21
    • 2018-09-10
    • 2018-05-15
    • 1970-01-01
    • 2021-07-22
    • 2019-08-27
    • 1970-01-01
    • 2020-12-19
    相关资源
    最近更新 更多