代码之家  ›  专栏  ›  技术社区  ›  paddy

双原子自旋锁的最小限制存储器排序

  •  8
  • paddy  · 技术社区  · 9 年前

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

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

    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 :具有此内存顺序的加载操作执行 获取操作 在受影响的内存位置:在加载之前,不能对当前线程中的内存访问进行重新排序。这确保了释放相同原子变量的其他线程中的所有写入在当前线程中可见。

    memory_order_release :具有此内存顺序的存储操作执行 释放操作 :当前线程中的任何内存访问都不能在此存储之后重新排序。这确保了当前线程中的所有写入在获取相同原子变量的其他线程中可见,并且将依赖关系写入到原子变量的写入在使用相同原子的其他线程中将可见。

    正如我所理解的那样,这足以在线程之间同步受保护的访问,以提供互斥行为,而不会对内存排序过于保守。

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

    呼叫 ++workers_busy , --workers_busy workers_busy.load() 所有当前都具有默认的存储顺序, memory_order_seq_cst 鉴于此原子唯一有趣的用途是解除阻塞 Modify() 具有 --工人_公共汽车 (这是 由自旋锁互斥体同步),是否可以使用相同的获取释放内存顺序,使用“松弛”增量来处理这个变量?

    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 );         // <--
    }
    

    这是正确的吗?有可能进一步放松这些记忆顺序吗?这有关系吗?

    2 回复  |  直到 5 年前
        1
  •  6
  •   ShadowRanger    9 年前

    Since you say you're targeting x86 only ,你是 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上,这会施加一些编译器排序约束;它不应该引入额外的围栏或不需要锁定的锁定指令。

        2
  •  -6
  •   Careful Now    9 年前

    您应该避免使用c++版本的测试并设置锁。相反,您应该使用编译器提供的原子指令。这实际上有很大的不同。这将与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,用于在执行该指令时锁定总线。这保证了原子读修改写。

    推荐文章