具有两个原子的自旋锁的最小限制内存排序

Least restrictive memory ordering for spin-lock with two atomics

我有一些工作线程定期(大约 1 kHz)执行时间关键处理。每个周期,工人们都会被叫醒做一件家务,每个应该(平均)在下一个周期开始之前完成。它们对同一个对象进行操作,主线程可以偶尔修改。

为了防止竞争,但允许在下一个周期之前修改对象,我使用了自旋锁和原子计数器来记录有多少线程仍在工作:

class Foo {
public:
    void Modify();
    void DoWork( SomeContext& );
private:
    std::atomic_flag locked = ATOMIC_FLAG_INIT;
    std::atomic<int> workers_busy = 0;
};

void Foo::Modify()
{
    while( locked.test_and_set( std::memory_order_acquire ) ) ;   // spin
    while( workers_busy.load() != 0 ) ;                           // spin

    // Modifications happen here ....

    locked.clear( std::memory_order_release );
}

void Foo::DoWork( SomeContext& )
{
    while( locked.test_and_set( std::memory_order_acquire ) ) ;   // spin
    ++workers_busy;
    locked.clear( std::memory_order_release );
    
    // Processing happens here ....

    --workers_busy;
}

这允许所有剩余的工作立即完成,前提是至少有一个线程已经开始,并且在另一个工作人员可以开始下一个周期的工作之前总是阻塞。

atomic_flag 通过“获取”和“释放”内存顺序访问,这似乎是使用 C++11 实现自旋锁的公认方式。根据 documentation at cppreference.com:

memory_order_acquire : A load operation with this memory order performs the acquire operation on the affected memory location: no memory accesses in the current thread can be reordered before this load. This ensures that all writes in other threads that release the same atomic variable are visible in the current thread.

memory_order_release : A store operation with this memory order performs the release operation: no memory accesses in the current thread can be reordered after this store. This ensures that all writes in the current thread are visible in other threads that acquire the same atomic variable and writes that carry a dependency into the atomic variable become visible in other threads that consume the same atomic.

据我了解,这足以跨线程同步受保护的访问以提供互斥行为,而不会过于保守内存排序。

我想知道是否可以进一步放宽内存排序,因为这种模式的副作用是我正在使用自旋锁互斥锁来同步另一个原子变量。

++workers_busy--workers_busyworkers_busy.load() 的调用目前都具有默认内存顺序 memory_order_seq_cst。鉴于此原子唯一有趣的用途是使用 --workers_busy 解除阻塞 Modify() (这是 not 由自旋锁互斥锁同步),是否可以获取相同的-释放内存顺序与此变量一起使用,使用“宽松”增量?

void Foo::Modify()
{
    while( locked.test_and_set( std::memory_order_acquire ) ) ;
    while( workers_busy.load( std::memory_order_acquire ) != 0 ) ;  // <--
    // ....
    locked.clear( std::memory_order_release );
}

void Foo::DoWork( SomeContext& )
{
    while( locked.test_and_set( std::memory_order_acquire ) ) ;
    workers_busy.fetch_add( 1, std::memory_order_relaxed );         // <--
    locked.clear( std::memory_order_release );
    // ....
    workers_busy.fetch_sub( 1, std::memory_order_release );         // <--
}

这是正确的吗?是否有可能进一步放宽这些内存排序中的任何一个?这有关系吗?

你应该避免使用c++版本的test和set lock。相反,您应该使用编译器提供的原子指令。这实际上有很大的不同。这将与 gcc 一起工作,并且是一个测试和测试并设置锁,它比标准测试和设置锁更有效。

unsigned int volatile lock_var = 0;
#define ACQUIRE_LOCK()   {                                                                           
                    do {                                                                    
                        while(lock_var == 1) {                                              
                            _mm_pause;                                                    
                        }                                                                   
                    } while(__sync_val_compare_and_swap(&lock_var, 0, 1) == 1);              
                }
#define RELEASE_LOCK()   lock_var = 0
//

_mm_pause 是英特尔推荐的处理器,因此有时间更新锁。

你的线程只有在获得锁后才会退出do while循环,然后进入临界区。

如果您查看 __sync_val_compare_and_swap 的文档,您会注意到它基于 xchgcmp 指令,并且在生成的程序集中其上方有单词 lock 以在执行此指令时锁定总线.这保证了原子读取修改写入。

, you're guaranteed strongly-ordered memory anyway;避免 memory_order_seq_cst 是有用的(它可以触发昂贵且不必要的内存栅栏),但除此之外,大多数其他操作不会强加任何特殊开销,因此除了允许可能不正确的编译器之外,您不会从额外的放松中获得任何好处指令重新排序。这应该是安全的,并且不会比任何其他使用 C++11 原子的解决方案慢:

void Foo::Modify()
{
    while( locked.test_and_set( std::memory_order_acquire ) ) ;
    while( workers_busy.load( std::memory_order_acquire ) != 0 ) ; // acq to see decrements
    // ....
    locked.clear( std::memory_order_release );
}

void Foo::DoWork( SomeContext& )
{
    while(locked.test_and_set(std::memory_order_acquire)) ;
    workers_busy.fetch_add(1, std::memory_order_relaxed); // Lock provides acq and rel free
    locked.clear(std::memory_order_release);
    // ....
    workers_busy.fetch_sub(1, std::memory_order_acq_rel); // No lock wrapping; acq_rel
}

最坏的情况是,在 x86 上,这会强加一些编译器顺序约束;它不应该引入额外的栅栏或不需要锁定的锁定指令。