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

时间:2021-04-06 16:51:44

标签: c# multithreading asynchronous .net-core thread-safety

我看到各地的人都建议尽可能使用 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) 那么,因为它引入了并行的可能性,需要使用锁等进行控制。

2 个答案:

答案 0 :(得分:2)

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

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

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

<块引用>

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

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

async Task FooAsync()
{
    ...
    Interlocked.Increment(ref counter);
    ...
}

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

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

答案 1 :(得分:2)

<块引用>

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

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

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

<块引用>

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

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

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