当前位置: 首页 > 知识库问答 >
问题:

流行的“不稳定轮询旗”模式被打破了吗?

长孙翔
2023-03-14

假设我想使用一个布尔状态标志来实现线程之间的协同取消。(我意识到最好使用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;
}

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

我看到的绝大多数消息来源表明,上述方法应该按预期工作;见: MSDN;乔恩·斯基特;布莱恩·吉迪恩;马克·格雷维尔;莱姆斯·鲁萨努。

但是,volatile仅在读取时生成获取界限,在写入时生成释放界限:

易失性读取具有“获取语义”;也就是说,在指令序列中,它保证发生在对内存的任何引用之前,而对内存的任何引用发生在它之后。易失性写入具有“释放语义”;也就是说,它保证在指令序列中写入指令之前的任何内存引用之后发生。(C#规格)

因此,正如Joseph Albahari所观察到的那样,无法保证易失性写入和易失性读取不会(似乎)交换。因此,在当前迭代结束后,后台线程可能会继续读取停止的过时值(即false)。具体地说,如果我在0s调用Start(),在3s调用Stop(),后台任务可能不会像预期的那样在10s终止,而是在20s或30s终止,或者根本不会终止。

基于获取和释放语义学,这里有两个问题。首先,易失性读取将被限制从内存中刷新字段(抽象地说)不是在当前迭代结束时,而是在后续迭代结束时,因为获取Geofence发生在读取本身之后。其次,更关键的是,没有什么可以强制易失性写入将值提交到内存中,因此根本无法保证循环会终止。

考虑以下顺序流:

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

最重要的部分是内存栅栏,标有-

附言:我知道MSDN对易失性关键字提供了非常强的保证。但是,专家的共识是MSDN是不正确的(并且没有ECMA规范的支持):

MSDN留档指出,使用易失性关键字“确保字段中始终存在最新的值”。这是不正确的,因为正如我们在前面的示例中看到的[],写后跟读可以重新排序。(Joseph Albahari)


共有1个答案

曾华翰
2023-03-14

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

是的,7秒绝对足够一个线程感知停止变量的变化。

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

在有限和有界的时间段内,将在其他线程中观察到来自一个线程的变量(具有特殊内存顺序)的任何更改。

如果没有这种保证,即使是变量的内存顺序特征也是无用的。

C#规范无疑为volatile变量提供了这样的保证,但我找不到对应的文本。

注意,关于有限时间的这种保证与记忆顺序保证(“获取”、“释放”等)无关,并且不能从障碍和记忆顺序的定义中推断出来。

什么时候说

我在3s时调用

其中一种暗示是,存在一些可见的效果(例如,打印到终端中的信息),这允许他声明大约3s的时间戳(因为打印语句是在Stop()之后发出的)。

随着C规范的优雅发挥(“10.10执行顺序”):

执行应继续进行,以便在关键执行点保留每个执行线程的副作用。副作用定义为对易失性字段的读取或写入、对非易失性变量的写入、对外部资源的写入以及引发异常。应保留这些副作用顺序的关键执行点是对易失性字段(§17.4.3)、锁语句(§15.12)以及线程创建和终止的引用。

假设打印是一个关键的执行点(很可能使用锁),那么您可以确信,在另一个线程可以看到分配给volatile变量的副作用,而另一个线程会检查给定的变量。

虽然允许编译器在代码中向前移动易失性变量的赋值,但它不能无限期地这样做:

>

  • 函数调用后无法移动赋值,因为编译器无法假设有关函数正文的任何内容。

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

    虽然人们可以想象代码具有1000个连续的简单赋值(对其他变量),因此可以为1000条指令解除易失性赋值,但编译器确实执行了这样的解除赋值。即使这样做,在现代CPU上执行1000条简单指令也不会超过几微秒。

    从CPU的角度来看,情况更简单:没有CPU会比有限数量的指令更推迟分配给存储单元。

    总的来说,对volatile变量的赋值只能在数量非常有限的指令上进行差异化。

  •  类似资料:
    • 问题内容: 下面的代码(几乎相同)在Linux下可以计算页面浏览量,但在Windows下可以将页面浏览量提高一倍。 有人能弄清楚为什么吗? 在Mingw下: 这可能是错误吗? 跟进: 实际上,如果我为另一个页面定义了其他处理程序,例如: Wich没有闭包,也没有增加任何东西,计数器无论如何都会增加,但是只有+1: 因此,仍然在Mingw的领导下: 在Linux下,输出如下所示: 问题答案: 我怀疑

    • 问题内容: 我的数据库中的名称记录偶尔包含撇号,例如,而我刚刚用PHP编写了一个查询脚本,该脚本捕获了该字段并将其粘贴到带有通常名称的select语句中,某些餐馆名称中的撇号使Love Train脱轨。 如何避免这种情况发生? 答案 -使用与通过PHP将它们插入数据库时​​使用的相同技术。 反驳 -然后我遇到了同样的问题,然后直接使用PHPMyAdmin作弊并输入了麻烦的问题,但这再也不能忽略了。

    • 我从geofabrik.de下载了我国家的OSM数据,成功地将其导入到安装在Ubuntu 16.04上的PostgreSQL 9.6中,并使用了几次。我还创建了Web应用程序,它可以正常工作。所以我决定添加另一个功能,从一些点返回最近的特殊点(例如餐馆)。对于一个最近的点,它可以工作,但是当我想要返回它们的数组时,它不工作。于是我分解了自己的问题,发现了奇怪的行为。当我执行以下查询: 它返回: 当

    • 问题内容: 当我尝试创建此错误时出现: 错误(E_UNKNOWN)::遇到意外错误MongoError:Server.insert(/ Users / oscargallon / Documents / developer / sails / reyesmagoswebpae / node_modules / sails- mongo / node_modules / mongodb / node

    • Java抛出某种类型的“Java.lang.IllegalAccessError类SomeJavassistProxy(在未命名模块中)无法访问类Java.lang.Reflect.Method(在模块Java.base中),因为模块Java.base不将Java.lang.Reflect导出到未命名模块”。 这是预期的行为。 然后我尝试了bytebuddy库,它生成了以下类:

    • 如果是这样,那怎么可能是封装呢?变量仍然是公共的。将其声明为private将迫使我创建一个setter和getter来获取变量,因为private可见性修饰符。 我将其与Java进行比较,后者通常将成员变量作为private以及每个成员变量的public setter和getter。