C# 是“大众化”吗;“易失性轮询标志”;图案破了?
假设我想在线程之间使用一个布尔状态标志进行协作取消。(我意识到最好使用C# 是“大众化”吗;“易失性轮询标志”;图案破了?,c#,.net,multithreading,volatile,memory-barriers,C#,.net,Multithreading,Volatile,Memory Barriers,假设我想在线程之间使用一个布尔状态标志进行协作取消。(我意识到最好使用CancellationTokenSource;这不是问题的重点。) 问题:如果我在另一个线程上在0s调用Start(),在3s调用Stop(),循环是否保证在当前迭代结束时在大约10s退出 我所看到的绝大多数资料表明,上述方法应该如预期的那样有效;见: ; ; ; ; . 但是,volatile仅在读取时生成获取围栏,在写入时生成释放围栏: 易失性读取具有“获取语义”;也就是说,在指令序列中,它保证发生在对内存的任何引
CancellationTokenSource
;这不是问题的重点。)
问题:如果我在另一个线程上在0s调用Start()
,在3s调用Stop()
,循环是否保证在当前迭代结束时在大约10s退出
我所看到的绝大多数资料表明,上述方法应该如预期的那样有效;见:
;
;
;
;
.
但是,volatile
仅在读取时生成获取围栏,在写入时生成释放围栏:
易失性读取具有“获取语义”;也就是说,在指令序列中,它保证发生在对内存的任何引用之前,而对内存的任何引用发生在它之后。
易失性写入具有“释放语义”;也就是说,它保证在指令序列中写入指令之前的任何内存引用之后发生。
()
因此,无法保证易失性写入和易失性读取不会(看起来)被交换,正如所观察到的那样。因此,在当前迭代结束后,后台线程可能会继续读取过时的\u stopping
(即false
)值。具体地说,如果我在0s调用Start()
,在3s调用Stop()
,后台任务可能不会像预期的那样在10s终止,而是在20s或30s终止,或者根本不会终止
基于此,这里有两个问题。首先,易失性读取将被限制从内存中刷新字段(抽象地说),而不是在当前迭代结束时,而是在后续迭代结束时,因为获取围栏发生在读取本身之后。第二,更关键的是,没有任何东西可以强制易失性写入将值提交到内存,因此无法保证循环将永远终止
考虑以下顺序流:
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 ------------
Time |线程1 |线程2
| |
0 | Start()调用:|读取_stopping的值
| | | ↑
|将“停止”设置为“真”↑
4 | ↓ | ↑
5 | ↓ | ↑
6 | ↓ | ↑
7 | ↓ | ↑
8 | ↓ | ↑
9 | ↓ | ↑
10 | ↓ | 读取_停止的值
| ↓ |
如果在另一个线程上,我在0s调用Start()
,在3s调用Stop()
,循环是否保证在当前迭代结束时以大约10s的速度退出
是的,7秒绝对足够一个线程感知到\u stopping
变量的变化
近乎正式的解释
对于提供任何类型的可见性屏障(内存顺序)的每个变量,任何语言的规范都应提供以下保证:
在finit和BOUND期间,将在其他线程中观察到来自一个线程的变量的任何变化(使用特殊的内存顺序)
如果没有这个保证,即使是变量的内存顺序特征也是无用的
C#规范肯定提供了关于volatile变量的这种保证,但我找不到对应的文本
注意,关于有限时间的这种保证与记忆顺序保证(“获取”、“释放”等)无关,不能从障碍和记忆顺序的定义中推断出它
正式非正式解释
什么时候说
我在3s时调用Stop()
一种暗示是,存在一些可见的效果(例如,打印到终端中的信息),这允许他声明大约3s的时间戳(因为print语句是在Stop()
之后发出的)
随着C#规范的优雅演奏(“10.10执行命令”):
执行应继续进行,以便在关键执行点保留每个执行线程的副作用。副作用定义为对易失性字段的读取或写入、对非易失性变量的写入、写入
并引发异常。应保留这些副作用顺序的关键执行点是对易失性字段(§17.4.3)、锁定语句(§15.12)和
线程的创建和终止
假设打印是一个关键执行点(很可能它使用锁),您可以确信,此时另一个线程可以看到对\u stopping
volatile变量的赋值,因为它会产生副作用
非正式解释
虽然编译器允许在代码中向前移动volatile变量的赋值,但它不能无限期地这样做:
- 赋值不能在函数调用后移动,因为编译器不能假定函数体的任何内容
- 如果分配在一个周期内执行,则应在下一个周期的另一次分配之前完成
- 虽然可以想象代码具有1000个连续的简单赋值(到其他变量),因此可以对1000条指令进行不稳定赋值,但编译器只是执行这种不稳定赋值。即使是这样,在现代CPU上执行1000条简单指令所需的时间也不超过几微秒
从CPU的角度来看,情况是simp
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 ------------