带有空主体的while循环检查易失性整数-这是啥意思?

Posted

技术标签:

【中文标题】带有空主体的while循环检查易失性整数-这是啥意思?【英文标题】:While loop with empty body checking volatile ints - what does this mean?带有空主体的while循环检查易失性整数-这是什么意思? 【发布时间】:2016-02-12 05:14:57 【问题描述】:

我正在查看具有以下几行的 C++ 类:

while( x > y );
return x - y;

xyvolatile int 类型的成员变量。我不明白这个结构。

我在这里找到了代码存根:https://gist.github.com/r-lyeh/cc50bbed16759a99a226。我猜它不能保证正确甚至有效。

【问题讨论】:

您确定您的转录正确(尤其是第一行末尾的分号)? 如果x > y你的代码会陷入死循环。 @Luca 该代码在什么环境中运行?是否是某个嵌入式控制器,其中 x 和 y 从外部更改(例如通过中断处理程序)? @Luca 转录不正确:原始有 while(x-y); return y-x; 并且只有 y 是易变的,而不是两者。但这不会改变接受的答案。 虽然以下答案可能是正确的,但我发现整个事情更有可能是个笑话。它以洛夫克拉夫特神话中的拉莱耶城命名。这座按照疯狂的原则建造的城市,光是看着就会让你发疯。 【参考方案1】:

由于xy 已被声明为易失性,程序员预计它们将从程序外部进行更改。

在这种情况下,您的代码将保留在循环中

 while(x>y);

并在值从外部更改为x <= y 后返回值x-y。在您告诉我们有关您的代码以及您在哪里看到它的更多信息后,可以猜到编写此代码的确切原因。在这种情况下,while 循环是一种等待其他事件发生的技术。

【讨论】:

也许值得注意的是,如果x - y 正在执行,x > y 可能再次为真(同时更改)。在进行所有这些检查和计算之前将其读入局部变量不是更安全吗? @Caramiriel:你是对的,但情况更糟。由于xy 是不同的变量,因此它们会单独更新和读取,并且声明它们volatile 不会改变这一点。因此,一个线程可能会以一种不打算改变x>y 的结果的方式同时更改两个变量,但是这个循环可能会读取一个中间状态,其中只有一个变量已被更新并且x>y 虚假地评估为假。当然,这在调试过程中永远不会发生…… 请注意,这对 CPU 使用率不利,因为它可能会立即重新迭代几秒钟。老实说,当前线程应该至少休眠一毫秒,这已经大大降低了 CPU 使用率。 但是,如果应用在微控制器上,代码就相当不错了。芯片可能太小而无法通过定时器中断进行适当的线程或调度。不确定这种方法的使用程度如何,但这绝对是可能的。 如果代码的其余部分提供了证明它是安全的所需的额外不变量,也可能没问题。作为一般规则,应该怀疑使用volatile 且没有其他同步工具的多线程代码。 volatile 所做的实际规则非常具体和细致入微,但许多开发人员认为它做了它实际上并没有做的事情。 (或者更确切地说,在这种特殊情况下,它确实在他们的特定编译器上完成了它们,所以他们永远不会学习!)【参考方案2】:

好像

while( x > y );

是spinning loop。直到x <= y 才会停止。由于xyvolatile,因此可以在此例程之外更改它们。因此,一旦x <= y 变为真,x - y 将被返回。此技术用于等待某个事件。

更新

根据您添加的the gist,似乎想法是实现线程安全的无锁循环缓冲区。是的,实现不正确。比如原来的代码-sn-p是

unsigned count() const 
    while( tail > head );
    return head - tail;

即使tail 变得小于或等于head,也不能保证head - tail 返回正数。调度程序可能会在while 循环之后立即将执行切换到另一个线程,并且该线程可能会更改head 的值。无论如何,还有很多其他问题与读取和写入共享内存的工作方式(内存重新排序等)有关,所以请忽略这段代码。

【讨论】:

我猜while( x > y ); 不保证根据目标平台的推荐做法生成旋转循环。例如,IA32 有一个PAUSE 指令,出于性能原因,建议所有旋转循环都包含该指令。但我想不能保证编译器会使用给定的源发出该指令。【参考方案3】:

其他回复已经详细指出了该指令的作用,但只是回顾一下,因为y(或链接示例中的head)被声明为volatile,对该变量进行了不同的更改一旦满足条件,线程将导致while 循环结束。

然而,尽管linked code example 很短,但它是一个近乎完美的例子,说明如何 编写代码。

首先是一行

while( tail > head );

会浪费大量的 CPU 周期,几乎会锁定一个内核直到满足条件。

随着我们的进展,代码会变得更好。

buffer[head++ % N] = item;

感谢 JAB 指出我将 post- 与 pre-increment 弄错了。更正了影响。 由于没有locks 或mutexes,我们显然必须假设最坏的情况。在分配item 中的值之后和head++ 执行之前,线程将切换。然后 Murphy 将再次调用包含该语句的函数,在相同的 head 位置分配 item 的值。 之后head 递增。现在我们切换回第一个线程并再次增加head。所以不是

buffer[original_value_of_head+1] = item_from_thread1; 
buffer[original_value_of_head+2] = item_from_thread2;

我们结束了

buffer[original_value_of_head+1] = item_from_thread2; 
buffer[original_value_of_head+2] = whatever_was_there_previously;

你可能会在客户端用很少的线程处理这种草率的编码,但在服务器端这只能被认为是一个定时炸弹。请改用locks 或mutexes 等同步构造。

好吧,为了完整起见,这条线

while( tail > head );

在方法pop_back()应该是

while( tail >= head );

除非您希望能够弹出比实际推入多一个的元素(或者甚至在推入之前弹出一个元素)。

很抱歉,我写的内容基本上可以归结为长篇大论,但如果这只是为了防止一个人复制和粘贴该淫秽代码,那是值得的。

更新:我想我不妨举一个例子,其中while(x>y); 这样的代码实际上非常有意义。 实际上,在过去的“美好时光”中,您曾经经常看到这样的代码。 咳嗽 DOS。 虽然没有在线程的上下文中使用。主要是在无法注册中断钩子的情况下作为后备(您的孩子可能会将其翻译为“无法注册事件处理程序”)。

startAsynchronousDeviceOperation(..);

这几乎可以是任何东西,例如告诉硬盘通过 DMA 读取数据,或告诉声卡通过 DMA 记录,甚至可能调用不同处理器(如 GPU)上的函数。通常通过outb(2) 之类的方式启动。

while(byteswritten==0); // or while (status!=DONE);

如果与设备的唯一通信渠道是共享内存,那就这样吧。不过,不希望在设备驱动程序和微控制器之外看到像现在这样的代码。显然假设规范声明内存位置是最后一个写入的位置。

【讨论】:

感谢理性的声音;意图对人类来说很清楚,但编译器不是人类...... 这是一个非常好的解构。谢谢你。 +1 如果线程 1 的 head++ 被评估上下文切换和线程 2 被隐含时,线程 1 中的项目将如何最终位于线程 2 中的项目的位置不是吗?然而,一个更微妙的问题是两个线程都获得了 head 的原始值(因为没有原子变量或锁 head++ 可能需要多个操作,通常涉及在递增它之前缓存该值,然后返回缓存的值) ,从而导致一个或另一个项目最终位于第一个位置,第二个位置保留其先前的值。 当然,即使具有原子性,您最终也可以在位置 2 中使用 item 1,在位置 1 中使用 item 2,但是如果您正在编写并发数据结构,您通常必须忍受这种情况。即使使用锁也会产生这种结果。 @JAB 头部,例如 0。T1 增加头部。切换到 T2。 T2 增加头部。 T2 使用 Heads 值 2 分配值。切换到 T1。 T1 使用 head = 2 的相同值分配其项目。所以是的,这是一个非常可能的场景。显然,在任何给定操作中都不太可能,但经常重复此操作(对于服务器端的共享内存结构来说是典型的),您最终会得到损坏的数据。【参考方案4】:

volatile 关键字旨在防止某些优化。在这种情况下,如果没有关键字,编译器可能会将您的 while 循环展开为具体的指令序列,这显然会在现实中中断,因为可以在外部修改值。

想象一下:

int i = 2;
while (i-- > 0) printf("%d", i);

大多数编译器会查看这一点并简单地生成两个对 printf 的调用 - 添加 volatile 关键字将导致它生成调用设置为 2 的计数器并在每次迭代后检查值的 CPU 指令.

例如,

volatile int i = 2;
this_function_runs_on_another_process_and_modifies_the_value(&i);
while(i-- > 0) printf("%d", i);

【讨论】:

this_function_runs_on_another_process_and_modifies_the_value(&i); 如果您正在执行多线程/多处理并且没有适当的内存围栏或其他保证顺序的方式,您需要atomic,而不是volatile(好吧,假设您关心订单;在某些情况下,轻松的订单很好)。在 C++(11) 中,编译器仍然可以对 volatile 变量的操作重新排序,只是不能删除任何加载或存储。 确实,但最初的问题是要求解释volatile - 对于任何需要同步值的实际应用程序,显然atomic 关键字是合适的。

以上是关于带有空主体的while循环检查易失性整数-这是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章

C++ 易失性成员函数

编译器可以通过指向易失性的指针优化存储吗? [复制]

在这个多线程 C++ 代码中是不是需要“易失性”?

将易失性数组与非易失性数组进行比较

易失性用户定义函数未按预期重新计算(VBA/Excel)

智能卡:非易失性存储器的状态已更改 - 0x6581