为何AsyncManualResetEvent的m_tcs需标记volatile?仅用CompareExchange不够吗?
我看到过Stephen Toub展示的AsyncManualResetEvent实现代码:
public class AsyncManualResetEvent { private volatile TaskCompletionSource<bool> m_tcs = new TaskCompletionSource<bool>(); public Task WaitAsync() { return m_tcs.Task; } public void Set() { m_tcs.TrySetResult(true); } public void Reset() { while (true) { var tcs = m_tcs; if (!tcs.Task.IsCompleted || Interlocked.CompareExchange(ref m_tcs, new TaskCompletionSource<bool>(), tcs) == tcs) return; } } }
我有个疑问:为什么要把m_tcs标记为volatile?只用Interlocked.CompareExchange来保证线程安全难道不够吗?
原因解析
解决内存可见性问题:如果不给
m_tcs加volatile修饰,多线程环境下,某线程对m_tcs的修改(比如Reset方法里替换成新的TaskCompletionSource实例)可能无法及时同步到主内存,其他线程读取时可能拿到缓存里的旧值。这会导致WaitAsync返回已完成的Task,或者Set操作作用在旧的tcs上,引发逻辑错误。Interlocked.CompareExchange不覆盖可见性:
Interlocked.CompareExchange只保证比较和替换操作的原子性,但管不了内存可见性。比如在Reset方法里执行var tcs = m_tcs;时,没有volatile的话,编译器或CPU可能会优化读取操作,从寄存器或本地缓存拿值,而不是主内存的最新值,后续的判断和原子操作就会基于过时的实例。volatile的核心作用:
volatile会强制编译器和CPU直接与主内存交互读写这个变量,禁止缓存优化,确保所有线程看到的m_tcs值都是最新的。这样Reset方法每次读取m_tcs都能拿到当前最新实例,WaitAsync和Set操作也能及时获取到正确的tcs对象,避免操作失效。
简单说,Interlocked.CompareExchange负责原子更新,volatile负责让所有线程看到这个更新,二者配合才能彻底保证线程安全。
内容的提问来源于stack exchange,提问作者Codisattva

