流行的“volatile polled flag”模式是否已经破坏了?
流行的“volatile polled flag”模式是否已经破坏了?
假设我想在线程之间使用布尔状态标志来协作取消操作。(我意识到最好使用CancellationTokenSource;但这不是问题的重点。)
私有的易变布尔变量 _stopping;
public void Start()
{
var thread = new Thread(() =>
{
while (!_stopping)
{
// 进行大约10秒钟的计算。
}
});
thread.Start();
}
public void Stop()
{
_stopping = true;
}
问题:如果我在另一个线程中在0秒调用Start(),并在3秒调用Stop(),循环是否保证在当前迭代结束时约为10秒的时候退出?
我看到的绝大多数来源都表明上述代码应该按预期工作;参见:
MSDN;
Jon Skeet;
Brian Gideon;
Marc Gravell;
Remus Rusanu。
然而,volatile只会在读操作时生成一个获取内存屏障,并在写操作时生成一个释放内存屏障:
“获取读操作具有‘获取语义’;也就是说,在指令序列中它保证在该读操作之后的所有内存引用之前发生。
释放写操作具有‘释放语义’;也就是说,它保证在该写操作之前的所有内存引用之后发生。”
(C#规范)
因此,无法保证易变写操作和易变读操作不会(表面上)被交换,正如Joseph Albahari所观察到的那样。因此,后台线程在当前迭代结束后可能会继续读取_stopping的旧值(即false)。具体来说,如果我在0秒调用Start(),并在3秒调用Stop(),后台任务可能不会像预期的那样在10秒时终止,而是在20秒、30秒甚至永远都不终止。
根据获取和释放语义,这里存在两个问题。首先,易变读操作将被限制为在当前迭代结束后而不是在随后的迭代结束时刷新字段(抽象地说,从内存中读取)。其次,更为关键的是,没有任何机制强制易变写操作将值提交到内存中,因此无法保证循环将会终止。
考虑以下序列流程:
时间 | 线程1 | 线程2
| |
0 | 调用Start(): | 读取_stopping的值
| | <----- 获取内存屏障 ------------
1 | |
2 | |
3 | 调用Stop(): | ↑
| ------ 释放内存屏障 ----------> | ↑
| 将_stopping设置为true | ↑
4 | ↓ | ↑
5 | ↓ | ↑
6 | ↓ | ↑
7 | ↓ | ↑
8 | ↓ | ↑
9 | ↓ | ↑
10 | ↓ | 读取_stopping的值
| ↓ | <----- 获取内存屏障 ------------
11 | ↓ |
12 | ↓ |
13 | ↓ | ↑
14 | ↓ | ↑
15 | ↓ | ↑
16 | ↓ | ↑
17 | ↓ | ↑
18 | ↓ | ↑
19 | ↓ | ↑
20 | | 读取_stopping的值
| | <----- 获取内存屏障 ------------
最重要的部分是内存屏障,标记为-->和<--,表示线程同步点。对于_stopping的易变读操作只能(表面上)移动到其线程的先前获取内存屏障。然而,易变写操作可以(表面上)无限地向下移动,因为它的后面没有其他释放内存屏障。换句话说,写入_stopping的操作与其任何读操作之间没有“同步-发生”(“先行发生”,“对其他线程可见”)的关系。
附:我知道MSDN对易变关键字给出了非常强的保证。然而,专家共识是MSDN是错误的(并且没有ECMA规范的支持):
“MSDN文档声称易变关键字“确保该字段的最新值始终存在”。这是错误的,因为正如我们在前面的示例中看到的,写操作之后的读操作可以被重新排序。”(Joseph Albahari)
“volatile polled flag”模式是否存在问题?
问题出现的原因:该问题的出现是由于对volatile变量的内存可见性的理解不清晰。虽然C#规范中提到了对volatile变量的内存可见性的保证,但具体的文本却没有找到。这导致了对volatile变量的内存可见性的保证存在疑问。
解决方法:根据C#规范中的“10.10 Execution order”部分,可以得出当在一个线程中调用Stop()
方法时,由于打印操作被认为是一个关键执行点,所以可以确保对_stopping
变量的赋值作为一个副作用是可见的,从而保证在其他线程中检查该变量时可以观察到变化。
此外,从编译器和CPU的角度来看,对volatile变量的赋值只能被推迟到非常有限的指令数量。因此,可以认为对volatile变量的赋值在有限的时间内是可见的。
总结起来,根据C#规范以及编译器和CPU的行为,可以解决“volatile polled flag”模式存在的问题,确保对volatile变量的内存可见性。