Dou*_*las 15 .net c# multithreading volatile memory-barriers
假设我想在线程之间使用布尔状态标志来进行协作取消.(我意识到最好使用一个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()0s和Stop()3s,那么循环是否保证在当前迭代结束时在10s左右退出?
我见过的绝大多数消息来源表明上述内容应该按预期工作; 见: MSDN ; Jon Skeet ; 布赖恩吉迪恩 ; 马克格拉维尔 ; Remus Rusanu.
但是,volatile只会在读取时生成获取栅栏,并在写入时生成释放栅栏:
易失性读取具有"获取语义"; 也就是说,保证在指令序列之后发生的任何内存引用之前发生.易失性写入具有"释放语义"; 也就是说,保证在指令序列中的写指令之前的任何存储器引用之后发生.(C#规格)
因此,正如Joseph Albahari所观察到的那样,无法保证不会(似乎)交换易失性写入和易失性读取.因此,后台线程可能会在当前迭代结束后继续读取_stopping(即false)的陈旧值.具体地说,如果我Start()在0和Stop()3s 调用,则后台任务可能不会按预期在10s终止,而是在20s,或30s,或者根本不会终止.
基于获取和释放语义,这里有两个问题.首先,易失性读取将被约束为从存储器中刷新字段(抽象地说)不是在当前迭代结束时,而是在后续迭代结束时,因为获取栅栏发生在读取本身之后.其次,更重要的是,没有什么可以迫使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.
PS我知道MSDN对volatile关键字提供了非常强有力的保证.但是,专家的共识是MSDN不正确(并且没有ECMA规范支持):
MSDN文档声明使用volatile关键字"确保始终在字段中存在最新值".这是不正确的,因为正如我们在前面的例子中看到的那样,可以重新排序写入后跟读取.(Joseph Albahari)
\n\n\n如果我在另一个线程上的
\nStart()0 秒和Stop()3 秒调用,循环是否保证在当前迭代结束时大约 10 秒退出?
是的,7秒对于一个线程感知_stopping变量的变化来说绝对足够了。
对于提供任何类型的可见性屏障(内存顺序)的每个变量,任何语言的规范都应提供以下保证:
\n\n\n\n\n在有限和有界的时间内,一个线程对变量(具有特殊内存顺序)的任何更改都将在其他线程中观察到。
\n
如果没有这个保证,即使变量的内存顺序特征也是无用的。
\n\nC# 规范确实提供了有关volatile变量的此类保证,但我找不到相应的文本。
\n\n请注意,这种关于有限时间的保证与内存顺序保证(“获取”、“释放”等)无关,并且不能从屏障和内存顺序的定义中推断出它。
\n\n当说
\n\n\n\n\n我
\nStop()3秒打电话
一个暗示,存在一些可见的效果(例如,信息打印到终端中),这允许他声称大约 3 秒的时间戳(因为 print 语句是在 后发出的Stop())。
使用该 C# 规范可以正常运行(“10.10 执行顺序”):
\n\n\n\n\n执行应继续进行,以便在关键执行点保留每个执行线程的副作用。副作用定义为读取或写入易失性字段、写入非易失性变量、写入外部资源以及引发异常。应保留这些副作用顺序的关键执行点是对易失性字段 (\xc2\xa717.4.3)、锁定语句 (\xc2\xa715.12) 和线程创建和终止的引用。
\n
假设打印是一个关键执行点(可能它使用锁),您可能确信此时对_stopping易失性变量的分配作为副作用对于检查给定变量的另一个线程是可见的。
虽然允许编译器在代码中向前移动volatile变量的赋值,但它不能无限期地这样做:
\n\n函数调用后不能移动赋值,因为编译器无法假设有关函数体的任何信息。
如果在一个周期内执行分配,则应在下一个周期的另一次分配之前完成。
虽然我们可以想象代码有 1000 个连续的简单赋值(对其他变量),因此 易失性赋值可以延迟 1000 条指令,但编译器只是执行这种延迟。即使是这样,在现代 CPU 上执行 1000 条简单指令也不会超过几微秒。
从CPU的角度来看,情况更简单:没有一个 CPU 会推迟对内存单元的分配超过有限数量的指令。
\n\n总的来说,对易失性变量的赋值只能在非常有限的指令上延迟。
\n| 归档时间: | 
 | 
| 查看次数: | 745 次 | 
| 最近记录: |