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