是流行的“易失性轮询标志"吗?模式坏了吗? [英] Is the popular "volatile polled flag" pattern broken?

查看:110
本文介绍了是流行的“易失性轮询标志"吗?模式坏了吗?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

假设我要使用布尔状态标志在线程之间进行协作取消. (我意识到最好使用CancellationTokenSource代替;这不是这个问题的重点.)

Suppose that I want to use a boolean status flag for cooperative cancellation between threads. (I realize that one should preferably use CancellationTokenSource instead; that is not the point of this question.)

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;
}

问题:如果我在另一个线程的0s调用Start(),在3s调用Stop(),是否保证循环在当前迭代结束时约10s退出?

Question: If I call Start() at 0s and Stop() at 3s on another thread, is the loop guaranteed to exit at the end of the current iteration at around 10s?

我所见的绝大多数资料表明,上述方法应能按预期工作;看: MSDN ; Jon Skeet Brian Gideon 马克·格雷夫(Marc Gravell) Remus Rusanu .

The overwhelming majority of sources I've seen indicate that the above should work as expected; see: MSDN; Jon Skeet; Brian Gideon; Marc Gravell; Remus Rusanu.

但是,volatile仅在读取时生成获取围栏,而在写入时生成释放围栏:

However, volatile only generates an acquire-fence on reads and a release-fence on writes:

易失性读取具有获取语义";也就是说,保证在指令序列中发生在对内存的任何引用之前. 易失性写入具有释放语义";即,保证在指令序列中的写入指令之前的任何存储器引用之后发生. ( C#规范)

A volatile read has "acquire semantics"; that is, it is guaranteed to occur prior to any references to memory that occur after it in the instruction sequence. A volatile write has "release semantics"; that is, it is guaranteed to happen after any memory references prior to the write instruction in the instruction sequence. (C# Specification)

因此,不能保证

Therefore, there is no guarantee that a volatile write and a volatile read will not (appear to) be swapped, as observed by Joseph Albahari. Consequently, it is possible that the background thread would keep reading the stale value of _stopping (namely, false) after the end of the current iteration. Concretely, if I call Start() at 0s and Stop() at 3s, it is possible that the background task will not terminate at 10s as expected, but at 20s, or 30s, or never at all.

基于获取和释放语义,这里有两个问题.首先,由于不是在当前迭代的末尾,而是在随后的 末尾,所以易失性读取将被约束从内存中刷新字段(抽象地讲),因为发生了获取栅栏之后读取本身.其次,更关键的是,没有什么可以迫使易失性写入将值提交到内存中,因此无法保证循环将永远终止.

Based on acquire and release semantics, there are two issues here. First, the volatile read would be constrained to refresh the field from memory (abstractly speaking) not at the end of the current iteration, but at the end of the subsequent one, since the acquire-fence occurs after the read itself. Second, more critically, there is nothing to force the volatile write to ever commit the value to memory, so there is no guarantee that the loop will ever terminate at all.

请考虑以下顺序流程:

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的写入与其任何读取之间的-before","is-visible-to"关系.

The most important parts are the memory fences, marked with --> and <--, which represent the thread synchronization points. The volatile read of _stopping can only (appear to) be moved up to its thread's previous acquire-fence at most. However, the volatile write can (appear to) be moved down indefinitely, since there is no other release-fence following it on its thread. In other words, there is no "synchronizes-with" ("happens-before", "is-visible-to") relation between the write to _stopping and any of its reads.

P.S.我知道MSDN在 volatile 关键字.但是,专家的共识是MSDN是不正确的(并且不受ECMA规范的支持):

P.S. I am aware that MSDN gives very strong guarantees on the volatile keyword. However, the expert consensus is that MSDN is incorrect (and not backed up by the ECMA spec):

MSDN文档指出,使用volatile关键字可确保字段中始终存在最新值".这是不正确的,因为正如我们在[上一个示例]中所看到的那样,可以对写入后进行读取的顺序进行重新排序. ( Joseph Albahari )

推荐答案

如果我在另一个线程的0s处调用Start(),在3s处调用Stop(),是否保证循环在当前迭代结束时约10s退出?

If I call Start() at 0s and Stop() at 3s on another thread, is the loop guaranteed to exit at the end of the current iteration at around 10s?

是的,一个线程感知到_stopping变量的更改绝对需要7秒.

Yes, 7 seconds is definitely sufficient for one thread to percieve change of _stopping variable.

对于提供任何类型的可见性障碍(内存顺序)的每个变量,任何语言的规范都应提供保证:

For every variable which provides any type of visibility barrier (memory order), specification for any language should provide a garantee that:

finit 有界时间段.

没有这种保证,即使变量的内存顺序功能也无济于事.

Without this garantee, even memory order features of the variable are useless.

C#规范肯定提供了有关 volatile 变量的保证,但我找不到对应的文本.

Specification for C# definitely provides such garantee about volatile variable, but I cannot find corresponded text.

请注意,有关finit时间的保证与内存顺序保证(获取",释放"等)无关,并且不能从障碍的定义中推论 和内存顺序.

Note, that such garantee about finit time is unrelated to memory orders garantees ("acquire", "release" and so on), and it cannot be deduced from the definitions of barriers and memory orders.

说话时

我在3秒后致电Stop()

一个暗示,存在某种可见效果(例如,打印到终端上的信息),这使他可以声明大约3 s的时间戳(因为打印声明是在之后发布的 Stop()).

one implies, that there was some visible effect (e.g., information printed into the terminal), which allows him to claim about 3s timestamp (because print statement has been issued after the Stop()).

随着C#规范的正常播放("10.10执行顺序"):

With that C# spec plays gracefully ("10.10 Execution order"):

执行过程应将每个执行线程的副作用保留在关键执行点上.副作用定义为对易失性字段的读取或写入,对非易失性变量的写入,写操作 外部资源,并引发异常.保留这些副作用的顺序的关键执行点是对易失性字段(第17.4.3节),锁语句(第15.12节)和 线程创建和终止.

Execution shall proceed such that the side effects of each executing thread are preserved at critical execution points. A side effect is defined as a read or write of a volatile field, a write to a non-volatile variable, a write to an external resource, and the throwing of an exception. The critical execution points at which the order of these side effects shall be preserved are references to volatile fields (§17.4.3), lock statements (§15.12), and thread creation and termination.

假设打印是一个关键执行点(可能使用锁),您可能会相信,目前分配给_stopping易失性变量作为副作用对另一个线程 visible 可见,该线程检查给定的变量.

Assuming printing is a critical execution point (likely it uses locks), you may be confident that at the moment assignment to _stopping volatile variable as a side effect is visible to the other thread, which checks given variable.

虽然允许编译器在代码中向前移动 volatile 变量的赋值,但它不能无限期地执行 :

While a compiler is allowed to move assignment of volatile variable forward in the code, it cannot do that indefinitely:

  • 在函数调用后不能移动分配,因为编译器无法假定有关函数主体的任何信息.

  • the assignment cannot be moved after the function call, because the compiler cannot assume anything about the function's body.

如果在一个周期内执行分配,则应在下一个周期的另一次分配之前完成该分配.

If assignment is performed inside a cycle, it should be completed before another assigment in the next cycle.

虽然可以想象具有1000个连续简单赋值(对其他变量)的代码,所以可以为1000条指令分配volatile赋值,但是编译器确实执行了这种赋值.即使这样做,在现代CPU上执行1000条简单指令也不会超过几微秒.

while one can imagine code with 1000 consecutive simple assignments (to other variables), so volatile assignment could be deffered for 1000 instructions, the compiler simply does perform such deffering. And even if it does, execution of 1000 simple instructions on modern CPU takes no more than several microseconds.

CPU 一侧来看,情况更简单:没有CPU会比分配给有限数量的指令更多地分配给存储单元.

From the side of a CPU, situation is simpler: none CPU will deffer assignment to memory cell more than limited number of instructions.

总计,仅在数量非常有限的指令上才可以分配 volatile 变量.

In total, assignment to volatile variable can be deffered only on very limited number of instructions.

这篇关于是流行的“易失性轮询标志"吗?模式坏了吗?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆