如何使一组语句在没有内存可见性效果的情况下成为原子操作?
如何使一组语句在没有内存可见性效果的情况下成为原子操作?
synchronized
块允许我将一组语句作为原子操作执行,同时确保块退出和进入之间存在happens-before关系。
我读到过,同步的最大代价是内存可见性的保证,而不是锁争用。
假设我能够通过其他方式保证内存可见性:
如何将一组语句作为原子操作执行,而不创建happens-before关系,即不使用synchronized
/Lock
的内存可见性效果?
我尝试使用CAS在用户空间实现锁,但内置的锁性能要远远优于它,并且CAS变量仍然会发出内存屏障。
在这个例子中,一个没有内存可见性效果的互斥锁就足够了。
(release/acquire) int x; // 具有release/acquire语义的变量 // release屏障 synchronized (this) { int y = x; // acquire屏障 // release屏障 x = 5; } // acquire屏障
相同的一组屏障会被发出两次(由互斥锁和x
)。
这会造成不必要的开销吗?
理论上是否可能存在没有内存效果的锁?
没有内存效果的锁是否实际上更高效?
在C++和/或Java中是否有内置的方法来实现这一点?
如果没有,是否可以在C++和/或Java中实现它?
在上述代码中,存在一个没有enter和exit之间happens-before关系的busy-waiting锁。为了解决这个问题,可以使用getAndSetRelease
方法来确立happens-before关系。然而,使用getAndSetRelease
方法会导致state
被无限缓存的问题。为了解决这个问题,可以使用opaque set/get方法。然而,Java中没有办法使用opaque语义来进行get-and-set操作。需要注意的是,getAndSetRelease()
方法使用普通语义读取值,并使用release语义写入值。
为了解决这个问题,可以使用下面的代码来实现busy-waiting锁,避免state
被无限缓存:
private static final VarHandle STATE = ...; private boolean state; void lock() { while ((boolean) STATE.getOpaque(this, true)) { while (state) { Thread.onSpinWait(); } } } void unlock() { STATE.setOpaque(this, false); }
在这个解决方案中,使用了getOpaque
和setOpaque
方法来确保操作的原子性,并避免state
被无限缓存。需要注意的是,Java中没有提供使用opaque语义进行get-and-set操作的方法。
以上是关于如何使一组语句具有原子性且没有内存可见性影响的问题出现的原因以及解决方法的整理。
如何使一组语句在没有内存可见性影响的情况下成为原子性的?
在使用互斥锁中,保证内存可见性的成本是可以忽略的,事实上,在x86上是免费的。获取互斥锁需要进行原子的读-修改-写操作,具有获取语义。释放互斥锁只需要使用简单的带有释放语义的存储操作。考虑一个简单的自旋锁,获取操作由一个循环组成,该循环不断尝试将锁标志设置为1,如果当前为0。释放锁,拥有锁的线程只需将锁标志写为0。在许多方面,这样简单的自旋锁远非最佳选择,有许多设计用于改进这一点(例如,公平性,自旋在本地缓存行上等),但在所有这些设计中,释放锁的成本肯定比获取锁更低。
x86内存模型非常强大:所有原子的读-修改-写操作都是顺序一致的,所有存储操作都具有有效的释放语义,所有加载操作都具有获取语义。这就是为什么在x86上释放互斥锁可以使用普通的存储操作,不需要额外的指令来确保内存效果的可见性。在像ARM或Power这样具有较弱内存模型的架构中,确实需要额外的指令,但与获取操作的成本相比,这些成本是可以忽略的。x86还具有特殊的屏障指令,但这些指令通常只在无锁编程的某些情况下相关,并且这些指令的成本与某些原子读-修改-写操作的成本大致相同。
互斥锁的真正成本不是内存效果的可见性,而是争用和执行的串行化。如果竞争互斥锁的线程数较少,并且线程持有互斥锁的持续时间也较短,则整体性能影响也较低。但是,如果竞争互斥锁的线程数量较大,并且线程持有互斥锁的持续时间也较长,则其他线程将不得不等待更长的时间,直到它们最终可以获取互斥锁并继续执行。这减少了在给定时间内可以执行的工作量。
“没有内存效果的锁是否在理论上是可能的?”我不确定你是什么意思。互斥锁的整个目的是允许一些操作被执行 - 并且被观察到 - 就好像它们是原子的一样。这意味着操作的效果对于互斥锁的下一个所有者是可见的。这实际上就是happens-before关系所保证的。如果线程A获取互斥锁,并且这个获取操作发生在某个线程B的释放操作之后,那么由于happens-before关系的传递性,线程B在持有互斥锁时执行的操作必须在线程A即将执行的操作之前发生 - 这意味着所有的内存效果必须是可见的。如果这不能得到保证,那么你的互斥锁就是有问题的,存在竞争条件。
关于你的示例中的volatile变量 - Java内存模型要求对共享的volatile变量的所有操作都是顺序一致的。但是,如果x只在临界区内访问(即受到某个互斥锁的保护),那么它就不需要是volatile的。只有当一些线程在没有任何其他同步机制(如互斥锁)的情况下访问变量时,才需要volatile。
互斥锁操作的释放/获取语义是为了对互斥锁内部的操作进行排序。在C++中,可以使用relaxed操作来实现一个互斥锁。互斥锁本身上的锁定/解锁操作仍然是完全有序的(由于互斥锁的修改顺序),但我们将失去happens-before关系,因此互斥锁内部的操作将是无序的。虽然在C++中这是可能的,但这实际上是荒谬的,因为正如我试图解释的,使内存效果可见非常廉价(在x86上是免费的),但你将失去在几乎所有情况下都至关重要的属性。注意:释放互斥锁的存储操作比对volatile变量的存储操作更便宜。volatile变量是顺序一致的,但释放互斥锁可以使用释放存储。
我知道在我的示例中,对x使用volatile语义是不需要的。我将其设置为volatile只是为了显示互斥锁的内存保证不是必需的情况。通过询问“理论上是否可能实现没有内存效果的锁”,我的意思是锁是否可以在不使用内存屏障的情况下实现,例如因为内存屏障用于在线程之间发布锁状态。
在关键部分的内存可见性由其他方式保证的情况下,您认为互斥锁没有happens-before关系就不足够吗?我认为这样的互斥锁可以仅用于保证原子性。
“为什么这实际上是荒谬的,因为你将失去在几乎所有情况下都至关重要的属性?”考虑在锁定互斥锁后以获取模式读取一个变量,并在解锁互斥锁之前以释放模式写入同一变量:在只提供了原子性保证的情况下,这是否足以创建happens-before关系并使互斥锁内部的操作有序化?
是的,这足够了,但是为什么你要这样做呢?你最终会增加两个额外的存储操作,只是为了实现与互斥锁相同的保证。
因为我需要在不持有锁的情况下读取变量。现在我将锁内部的读取操作设置为普通模式,但我意识到如果我能够去除互斥锁的内存保证,我也可以将其保持为获取模式。因此,我提出了这个问题。
是的,理论上可以使用松散操作来实现一种“类似锁”的机制,但这几乎没有任何好处(如果有的话 - 再次强调,在x86上你实际上没有任何好处)。如果你发现自己在锁中使用volatile变量,可能有一种可能性,要么变量实际上不需要是volatile的(因为它从不在锁之外访问),要么你可能能够完全摆脱锁。是否可能取决于具体情况。
在你上面的示例中,你可以使用AtomicInt和getAndSet来避免使用锁,但我想你的实际用例可能不是那么简单。但是,如果你更详细地描述你的实际用例,我可能能够提供帮助。
当然,那只是一个例子。如果你对我的答案满意,如果你能接受它,那就太好了。)
所以你基本上是在尝试实现一个并发队列。你的要求是什么?单生产者/单消费者(SPSC),多生产者/多消费者(MPMC)还是混合(MPSC/SCMP)?它应该是有界的还是无界的?你是否需要阻塞操作,还是tryPop就足够了(即,在队列为空时返回false)。但是,这个讨论可能超出了最初的问题范围。你可能想开一个新的问题?
当然,让我们通过电子邮件继续讨论。但是如果你对我的答案满意,如果你能接受它,那就太好了。;)
如何使一组语句成为原子操作而不产生内存可见性效应?
在上述内容中,出现了这个问题的原因是因为有人在寻找一种方法来使一组语句成为原子操作,而不会产生内存可见性的影响。为了解决这个问题,有一些解决方法被提出。
一个例子是针对热循环需要检查原子变量的使用情况。解决方法是每隔1024次迭代才检查一次。之所以选择1024,是因为它是2的幂,所以MOD运算符会被优化为快速的按位与计算。其他的2的幂也可以使用,根据需要进行调整。这样,在循环执行的过程中,原子操作的开销相对于循环执行的工作来说是可以忽略不计的。
除了这个例子,还提到了其他用例的解决方法,以及关于锁定内存效果的理论可能性和实际可行性的讨论。
由于硬件中断和软件中断是完全不同的,所以在理论上是可能实现没有内存效果的锁定的。但在实际应用中,这对于高级语言如C++或Java以及高级操作系统如Linux或Windows的用户态进程来说可能并不适用。只有在使用嵌入式操作系统如QMX或编写Windows或Linux的内核模式设备驱动程序时才可能实现。因此,一个合理的经验法则是假设所有的锁都有内存效果。如果担心性能问题,可以运行性能分析器,选择适合的线程架构。
总结起来,从这些内容中可以得出,使一组语句成为原子操作而不产生内存可见性效应的解决方法是检查频率限制、使用硬件中断和选择合适的线程架构。尽管目前在高级语言中无法实现没有内存效果的锁定,但理论上是可能的,特别是在嵌入式操作系统或内核模式设备驱动程序的应用中。