假设我想在线程之间使用布尔状态标志进行协作取消。 (我意识到应该最好使用CancellationTokenSource
;这不是这个问题的重点。)
private volatile bool _stopping;
public void Start()
{
var thread = new Thread(() =>
{
while (!_stopping)
{
// Do computation lasting around 10 seconds.
}
});
thread.Start();
}
public void Stop()
{
_stopping = true;
}
问题:如果我在另一个线程上调用Start()
0和Stop()
3s,那么循环是否保证在当前迭代结束时以10s左右退出?
我见过的绝大多数消息来源表明上述内容应该按预期工作;看到: MSDN; Jon Skeet; Brian Gideon; Marc Gravell; Remus Rusanu。
但是,volatile
仅在读取时生成获取栅栏,在写入时生成释放栅栏:
易失性读取具有“获取语义”;也就是说,保证在指令序列之后发生的任何内存引用之前发生。 易失性写入具有“释放语义”;也就是说,保证在指令序列中的写指令之前的任何存储器引用之后发生。 (C# Specification)
因此,无法保证不会(似乎)交换易失性写入和易失性读取,如Joseph Albahari所观察到的那样。因此,后台线程可能会在当前迭代结束后继续读取_stopping
的陈旧值(即false
)。具体来说,如果我在0s调用Start()
而在3s调用Stop()
,则后台任务可能不会按预期在10s终止,而是在20s,或30s,或者根本不会终止。
基于acquire and release semantics,这里有两个问题。首先,易失性读取将被约束为从内存刷新字段(抽象地说)不是在当前迭代结束时,而是在后续结束时刷新字段,因为获取栅栏发生< em>在读取之后。其次,更重要的是,没有什么可以强制volatile写入将值提交给内存,因此无法保证循环将永远终止。
考虑以下顺序流程:
Time | Thread 1 | Thread 2
| |
0 | Start() called: | read value of _stopping
| | <----- acquire-fence ------------
1 | |
2 | |
3 | Stop() called: | ↑
| ------ release-fence ----------> | ↑
| set _stopping to true | ↑
4 | ↓ | ↑
5 | ↓ | ↑
6 | ↓ | ↑
7 | ↓ | ↑
8 | ↓ | ↑
9 | ↓ | ↑
10 | ↓ | read value of _stopping
| ↓ | <----- acquire-fence ------------
11 | ↓ |
12 | ↓ |
13 | ↓ | ↑
14 | ↓ | ↑
15 | ↓ | ↑
16 | ↓ | ↑
17 | ↓ | ↑
18 | ↓ | ↑
19 | ↓ | ↑
20 | | read value of _stopping
| | <----- acquire-fence ------------
最重要的部分是内存栅栏,标有-->
和<--
,代表线程同步点。 _stopping
的易失性读取只能(似乎)最多移动到其线程的先前获取范围。但是,易失性写入可以(似乎)无限期地向下移动,因为在其线程上没有其他释放栅栏。换句话说,写入_stopping
及其任何读取之间没有“synchronizes-with”(“发生前”,“是 - 可见 - ”)关系。
P.S。我知道MSDN对volatile
关键字提供了非常强大的保证。但是,专家的共识是MSDN不正确(并没有得到ECMA规范的支持):
MSDN文档指出使用volatile关键字“确保始终在字段中存在最新值”。这是不正确的,因为正如我们在前面的例子中看到的那样,可以重新排序写入后跟读取。 (Joseph Albahari)
答案 0 :(得分:1)
如果我在另一个线程上调用
Start()
为0,Stop()
为3,那么循环是否保证在当前迭代结束时以10s左右退出?
是的,对于一个线程来说,只需7秒即可完成_stopping
变量的更改。
对于提供任何类型可见性障碍(内存顺序)的每个变量,任何语言的规范都应该提供以下保证:
在 finit <期间,其他线程中观察来自一个线程的变量(具有特殊内存顺序)的更改 / strong>和有界一段时间。
如果没有这个保证,即使变量的记忆顺序功能也没用。
C#规范明确提供了关于 volatile 变量的保证,但我找不到相应的文本。
请注意,关于finit时间的这种保证与记忆订单保证(“获取”,“发布”等)无关,并且无法从障碍的定义中推断 和记忆订单。
当说
我在3点打电话给
Stop()
一个暗示,有一些可见效果(例如,打印到终端的信息),这允许他声称大约3s时间戳(因为打印声明已经发布之后 Stop()
)。
使用C#规范优雅地播放(“10.10执行顺序”):
执行应继续进行,以便在关键执行点保留每个执行线程的副作用。副作用定义为易失性字段的读取或写入,对非易失性变量的写入,写入 到外部资源,抛出异常。应保留这些副作用顺序的关键执行点是对volatile字段(第17.4.3节),锁定语句(第15.12节)和 线程创建和终止。
假设打印是关键执行点(可能它使用锁定),您可能会确信此时将_stopping
volatile变量指定为副作用是可见到另一个线程,它检查给定的变量。
虽然允许编译器在代码中向前移动 volatile 变量的分配,但它无法无限期:
在函数调用之后无法移动赋值,因为编译器不能假设函数体的任何内容。
如果在一个循环内执行分配,则应在下一个循环中的另一个分配之前完成分配。
虽然可以想象具有1000个连续简单赋值(对于其他变量)的代码,因此可以针对1000个指令来定义易失性赋值,编译器只是执行这样的deffering。即使这样,在现代CPU上执行1000条简单指令也不会超过几微秒。
从 CPU 的一侧,情况更简单:没有CPU会比有限数量的指令更多地分配给内存单元。
总计, volatile 变量的分配只能在非常有限的指令数上进行。