我看到各地的人都建议尽可能使用 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)
那么,因为它引入了并行的可能性,需要使用锁等进行控制。
答案 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
确实引入了可重入的可能性,但真正引起问题的情况很少见。异步代码本质上鼓励更多功能的结构(方法的输入是它的参数,输出是它的返回值)。异步方法可能有副作用并依赖于状态,但这并不常见。
请注意,导致意外重入的是 await
。 ConfigureAwait(false)
在线程池上恢复,但这不会导致这里的问题。
如果一个 UI 线程不断调用这个 FooAsync(例如因为用户按下按钮),这段代码不会破坏计数器的值并保存文件吗?由于多个线程可能正在执行?
是的,有点。是的,计数器可能会得到一个意想不到的值,但这不一定是因为多线程。考虑没有 ConfigureAwait(false)
的相同代码:您仍然在单个线程上运行该函数的多个调用。他们仍在争夺柜台和任何其他共享状态。在这种情况下,由于是单线程,counter++
是原子的,但由于它是共享的,从 await
恢复时,对该函数的单次调用可能会看到值意外更改。
使用ConfigureAwait(false)
,您确实会担心意外并行(使用await
,您会意外重入 >),所以如果你有非线程安全的共享状态,事情会变得更糟。重入会导致意外状态,但并行会导致无效状态。