您可以在没有线程安全的情况下使用 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
确实引入了重入的可能性,但它实际上导致问题的情况很少见。异步代码本质上鼓励一种功能性更强的结构(方法的输入是它的参数,输出是它的返回值)。 可能异步方法有副作用并依赖于状态,但这并不常见。
请注意,导致意外重入的是await
。 ConfigureAwait(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) 吗?的主要内容,如果未能解决你的问题,请参考以下文章