除了最简单的自旋锁算法外,互斥代码也相当复杂:一个好的优化互斥锁/解锁代码包含了即使是优秀的程序员也难以理解的代码。它使用特殊的比较和设置指令,不仅管理解锁/锁定状态,还管理等待队列,可选择使用系统调用进入等待状态(用于锁定)或唤醒其他线程(用于解锁)。
无论如何,一般的编译器都无法解码和“理解”所有复杂的代码(同样,除了简单的自旋锁),所以即使对于不知道互斥锁是什么以及它如何相关的编译器来说对于同步,实际上编译器无法围绕此类代码优化任何内容。
那是如果代码是“内联的”,或者可用于分析以进行跨模块优化,或者是否可以使用全局优化。
我假设编译器实际上并不理解 pthread_mutex_lock() 是一个特殊函数,所以我们只是受到序列点的保护吗?
编译器不知道它做了什么,所以不会尝试围绕它进行优化。
怎么“特别”?它是不透明的并被这样对待。在不透明函数中并不特殊。
可以访问任何其他对象的任意不透明函数没有语义差异。
我关心的是缓存。编译器能否将 _protected 的副本放在堆栈或寄存器中,并在分配中使用该陈旧值?
是的,在透明而直接地作用于对象的代码中,通过以编译器可以遵循的方式使用变量名或指针。不在可能使用任意指针间接使用变量的代码中。
所以在对不透明函数的调用之间是的。不跨越。
并且对于只能在函数中使用的变量,按名称:对于没有获取地址或绑定到它们的引用的局部变量(这样编译器就无法遵循所有进一步的用途)。这些确实可以跨任意调用“缓存”,包括锁定/解锁。
如果没有,是什么阻止了这种情况的发生?这种模式的变体是否容易受到攻击?
函数的不透明度。非内联。汇编代码。系统调用。代码复杂度。使编译器摆脱困境并认为“这是复杂的东西只需调用它”的一切。
编译器的默认位置始终是“让我们愚蠢地执行我不明白正在做什么”而不是“我将优化它/让我们重写我更了解的算法”。大多数代码没有以复杂的非本地方式进行优化。
现在让我们假设绝对更糟(从编译器应该放弃的角度来看,从优化算法的角度来看这是绝对最好的):
- 该函数是“内联的”(= 可用于内联)(或全局优化启动,或者所有函数在道德上都是“内联”的);
- 在该同步原语(锁定或解锁)中不需要内存屏障(如在单处理器时间共享系统和多处理器强排序系统中),因此它不包含此类内容;
- 没有使用特殊指令(如比较和设置)(例如对于自旋锁,解锁操作是简单的写入);
- 没有系统调用来暂停或唤醒线程(在自旋锁中不需要);
那么我们可能会遇到问题,因为编译器可以围绕函数调用进行优化。这可以通过插入编译器屏障(例如带有“clobber”的空 asm 语句以用于其他可访问变量)来轻松解决。这意味着编译器只是假设被调用函数可以访问的任何东西都被“破坏”了。
或者受保护的变量是否需要是易失的。
您可以将其设置为 volatile 的原因通常是使事情变得 volatile:确保能够访问调试器中的变量,防止浮点变量在运行时具有错误的数据类型,等等。
使其 volatile 实际上甚至无法解决上述问题,因为volatile 本质上是抽象机器中的内存操作,具有 I/O 操作的语义,因此仅针对以下内容进行排序
- 像 iostream 这样的真实 I/O
- 系统调用
- 其他不稳定的操作
- asm memory clobbers(但是没有围绕这些重新排序的内存副作用)
- 调用外部函数(因为它们可能会执行上述操作之一)
就非易失性内存副作用而言,易失性没有排序。这使得 volatile对于编写线程安全代码实际上是无用的(对于实际用途无用),即使是在 volatile 会先验帮助的最具体情况下,也就是不需要内存围栏的情况:在时间共享系统上编写线程原语时单CPU。(这可能是 C 或 C++ 中最不被理解的方面之一。)
因此,虽然 volatile 确实可以防止“缓存”,但volatile 甚至不会阻止编译器重新排序锁定/解锁操作,除非所有共享变量都是 volatile。