我有一些工作线程以固定间隔(约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;
}
这允许所有剩余的工作立即完成,前提是至少有一个线程已经开始,并且在另一个工作人员开始下一个周期的工作之前总是会阻塞。
使用&#34;获取&#34;访问atomic_flag
和&#34;发布&#34;内存命令,似乎是用C ++ 11实现自旋锁的可接受方式。根据{{3}}:
memory_order_acquire
:具有此内存顺序的加载操作会在受影响的内存位置执行获取操作:当前线程中的内存访问不能重新排序在此负载之前。这可以确保在当前线程中可以看到释放相同原子变量的其他线程中的所有写入。
memory_order_release
:具有此内存顺序的存储操作会执行发布操作:在此存储之后,当前线程中的内存访问不能重新排序。这可以确保当前线程中的所有写入在获取相同原子变量的其他线程中可见,并且带有依赖关系到原子变量的写入在消耗相同原子的其他线程中变得可见。
据我所知,这足以跨线程同步受保护的访问以提供互斥行为,而不会对内存排序过于保守。
我想知道的是内存排序是否可以进一步放宽,因为这种模式的副作用是我使用自旋锁互斥来同步另一个原子变量。
对++workers_busy
,--workers_busy
和workers_busy.load()
的调用目前都有默认的内存顺序memory_order_seq_cst
。鉴于此原子的唯一有趣用途是使用Modify()
取消阻止--workers_busy
(旋转锁定互斥锁不同步),可以使用相同的获取 - 释放内存订单与此变量一起使用,使用&#34;放松&#34;增量? 即
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 ); // <--
}
这是对的吗?是否有可能进一步放宽这些内存排序?它甚至重要吗?
答案 0 :(得分:5)
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上,这会产生一些编译器排序限制;它不应该引入额外的围栏或锁定指令,不需要锁定。
答案 1 :(得分:-5)
您应该避免使用测试的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来锁定总线,同时执行此指令。这保证了原子读取修改写入。