带有空主体的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;
x
和y
是volatile int
类型的成员变量。我不明白这个结构。
我在这里找到了代码存根:https://gist.github.com/r-lyeh/cc50bbed16759a99a226。我猜它不能保证正确甚至有效。
【问题讨论】:
您确定您的转录正确(尤其是第一行末尾的分号)? 如果x > y
你的代码会陷入死循环。
@Luca 该代码在什么环境中运行?是否是某个嵌入式控制器,其中 x 和 y 从外部更改(例如通过中断处理程序)?
@Luca 转录不正确:原始有 while(x-y); return y-x;
并且只有 y
是易变的,而不是两者。但这不会改变接受的答案。
虽然以下答案可能是正确的,但我发现整个事情更有可能是个笑话。它以洛夫克拉夫特神话中的拉莱耶城命名。这座按照疯狂的原则建造的城市,光是看着就会让你发疯。
【参考方案1】:
由于x
和y
已被声明为易失性,程序员预计它们将从程序外部进行更改。
在这种情况下,您的代码将保留在循环中
while(x>y);
并在值从外部更改为x <= y
后返回值x-y
。在您告诉我们有关您的代码以及您在哪里看到它的更多信息后,可以猜到编写此代码的确切原因。在这种情况下,while
循环是一种等待其他事件发生的技术。
【讨论】:
也许值得注意的是,如果x - y
正在执行,x > y
可能再次为真(同时更改)。在进行所有这些检查和计算之前将其读入局部变量不是更安全吗?
@Caramiriel:你是对的,但情况更糟。由于x
和y
是不同的变量,因此它们会单独更新和读取,并且声明它们volatile
不会改变这一点。因此,一个线程可能会以一种不打算改变x>y
的结果的方式同时更改两个变量,但是这个循环可能会读取一个中间状态,其中只有一个变量已被更新并且x>y
虚假地评估为假。当然,这在调试过程中永远不会发生……
请注意,这对 CPU 使用率不利,因为它可能会立即重新迭代几秒钟。老实说,当前线程应该至少休眠一毫秒,这已经大大降低了 CPU 使用率。
但是,如果应用在微控制器上,代码就相当不错了。芯片可能太小而无法通过定时器中断进行适当的线程或调度。不确定这种方法的使用程度如何,但这绝对是可能的。
如果代码的其余部分提供了证明它是安全的所需的额外不变量,也可能没问题。作为一般规则,应该怀疑使用volatile
且没有其他同步工具的多线程代码。 volatile
所做的实际规则非常具体和细致入微,但许多开发人员认为它做了它实际上并没有做的事情。 (或者更确切地说,在这种特殊情况下,它确实在他们的特定编译器上完成了它们,所以他们永远不会学习!)【参考方案2】:
好像
while( x > y );
是spinning loop。直到x <= y
才会停止。由于x
和y
是volatile
,因此可以在此例程之外更改它们。因此,一旦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 弄错了。更正了影响。
由于没有lock
s 或mutex
es,我们显然必须假设最坏的情况。在分配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;
你可能会在客户端用很少的线程处理这种草率的编码,但在服务器端这只能被认为是一个定时炸弹。请改用lock
s 或mutex
es 等同步构造。
好吧,为了完整起见,这条线
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循环检查易失性整数-这是啥意思?的主要内容,如果未能解决你的问题,请参考以下文章