您可以在没有线程安全的情况下使用 ConfigureAwait(false) 吗?

Posted

技术标签:

【中文标题】您可以在没有线程安全的情况下使用 ConfigureAwait(false) 吗?【英文标题】:Can you use ConfigureAwait(false) without being thread-safe? 【发布时间】:2021-07-02 11:55:12 【问题描述】:

我看到各地的人都建议尽可能使用ConfigureAwait(false),这是图书馆作者必须使用的,等等。

但是由于ConfigureAwait(false) 的延续可以在线程池中的任何线程上运行,那么如何安全地防止多个线程访问库中的相同状态?

假设您的库有以下 API:

async Task FooAsync()

    // Do something
    
    //barAsync and saveToFileAsync are private methods.
    await barAsync().ConfigureAwait(false);
    
    // counter is a private field
    counter++;

    await saveToFileAsync().ConfigureAwait(false);
    
    // Do other things

如果 UI 线程一直调用此 FooAsync(例如,由于用户按下按钮),此代码不会损坏 counter 的值并保存文件吗?由于可能正在执行多个线程?

除了不修改状态的最简单的情况外,我发现很难在没有线程安全的情况下使用 ConfigureAwait(false)

更新

我可能不清楚,但在我们的团队中,我们决定使用单线程。因此,从下面的答案来看,我们似乎不能使用ConfigureAwait(false),因为它引入了并行的可能性,需要使用锁等来控制。

【问题讨论】:

你不能。不知道还能说什么。即使在没有使用 ConfigureAwait 的情况下,您的代码也不是线程安全的,因为您无法保证调用者一开始就在 UI 线程上。 那时你可能需要考虑锁定 只要代码不是上下文感知的,建议使用 ConfigureAwait(false)。一个示例是 asp .NET 应用程序中的 HttpContext。您不能从请求线程以外的线程访问它。 @Lasse 你是对的。大多数人不是为线程安全而设计的,他们认为只有一个线程在运行,这是默认的。普通的 async/await 很适合这个,但是,当你添加 ConfigureAwait(false) 时,你需要线程安全,但是在我访问的大多数问题中似乎没有人解决这个问题,它给人的印象是 ConfigureAwait(false)是一个简单的更改,但不,它有后果并可能导致损坏状态。 Most people don't design for thread-safety 我不认为这是真的。大多数 good 库都是线程安全的,因为存在的小全局状态被锁定。除非特别说明(例如Concurrent... 类),否则不期望单个对象应该是线程安全的。例如,List<T> 作为单个对象不是线程安全的。 SqlConnection 不是线程安全的,而是使用内部连接池。 【参考方案1】:

但由于 ConfigureAwait(false) 的延续可以在线程池中的任何线程上运行,那么如何安全地防止多个线程访问库中的相同状态?

await 确实引入了重入的可能性,但它实际上导致问题的情况很少见。异步代码本质上鼓励一种功能性更强的结构(方法的输入是它的参数,输出是它的返回值)。 可能异步方法有副作用并依赖于状态,但这并不常见。

请注意,导致意外重入的是awaitConfigureAwait(false) 在线程池中恢复,但这不会导致这里出现问题。

如果 UI 线程不断调用此 FooAsync(例如,由于用户按下按钮),此代码不会损坏计数器的值并保存文件吗?由于可能正在执行多个线程?

是的,有点像。是的,计数器可能会得到一个意外的值,但这不一定是因为多个线程。考虑没有ConfigureAwait(false) 的相同代码:您仍然可以在单个线程上运行该函数的多个调用。他们仍在为柜台和任何其他共享状态而战。在这种情况下,由于是单线程,counter++ 是原子的,但由于它是共享的,因此从 await 恢复时,对该函数的单次调用可能会看到值意外更改。

使用ConfigureAwait(false),你确实有额外的担心并行(使用await你有意外重入),所以如果你有非线程安全的共享状态,事情会变得更糟。重入会导致意外状态,但并行性会导致无效状态。

【讨论】:

【参考方案2】:

ConfigureAwait 与线程安全无关。这是关于避免捕获上下文。

如果您希望您的代码是线程安全的,那么您应该实现它。这通常涉及使用某种同步结构,例如锁。

正如已经指出的那样,即使您删除了对 ConfigureAwait(false) 的调用,您的 FooAsync()不是线程安全的。即使在有SynchronizationContext 可用的UI 应用程序中,两个或多个线程仍然可以同时调用它。

如何安全地防止多个线程访问库中的同一状态?

通过同步对任何共享资源的访问。假设counter 是您代码中唯一的关键部分,您可以使用Interlocked.Increment API 使方法线程安全:

async Task FooAsync()

    ...
    Interlocked.Increment(ref counter);
    ...

这将增加counter 并将新结果存储为原子操作。

还有很多其他的同步结构。使用哪一个取决于您基本上在做什么。避免调用ConfigureAwait(false) 不是让代码线程安全的方法。

【讨论】:

谢谢,@mm8。我想我可能没有说清楚。如果在我们的团队中我们决定使用单线程,那么ConfigureAwait(false) 似乎打破了这个角色。至于FooAsync(),保证会从UI线程调用。

以上是关于您可以在没有线程安全的情况下使用 ConfigureAwait(false) 吗?的主要内容,如果未能解决你的问题,请参考以下文章

python哪些容器线程安全

HashMap 对于不同的键是线程安全的吗?

线程安全的 C++ 堆栈

线程安全—原子性

Cipher 线程安全吗?

在没有内核支持的情况下唤醒线程