两种std::atomic_flag初始化与使用方法的优劣对比及技术疑问
关于两种atomic_flag线程通知方法的分析与疑问
我看到两种基于std::atomic_flag的线程通知实现写法:
- METHOD 1:在
main()中将两个flag设为true,线程调用wait()等待true状态; - METHOD 2:main中不预先设置flag,线程等待
false状态。
当main将第一个flag设为对应状态后,线程被唤醒,执行clear()或test_and_set(),随后进入临界区递增共享变量data。两种方法本质都是线程间的通知机制,仅flag的初始状态和等待条件相反,最终main都能返回预期值13。
METHOD 1
#include <atomic> #include <thread> #include <functional> void func1( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( true ); input_data_flag.test_and_set( std::memory_order_acquire ); data += 1; // Prepare the second flag and notify the second thread output_data_flag.clear( std::memory_order_release ); output_data_flag.notify_one( ); } void func2( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( true ); input_data_flag.test_and_set( std::memory_order_acquire ); data += 2; output_data_flag.clear( std::memory_order_release ); output_data_flag.notify_one( ); } int main( ) { std::atomic_flag flag1 { }; std::atomic_flag flag2 { }; flag1.test_and_set( std::memory_order_acquire ); flag2.test_and_set( std::memory_order_acquire ); int data { 10 }; { std::jthread func1_thread { func1, std::ref( data ), std::ref( flag1 ), std::ref( flag2 ) }; std::jthread func2_thread { func2, std::ref( data ), std::ref( flag2 ), std::ref( flag1 ) }; // here the first thread is finally notified to start its task flag1.clear( std::memory_order_release ); flag1.notify_one( ); } return data; }
METHOD 2
#include <atomic> #include <thread> #include <functional> void func1( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( false ); input_data_flag.clear( std::memory_order_release ); data += 1; // Prepare the second flag and notify the second thread output_data_flag.test_and_set( std::memory_order_acquire ); output_data_flag.notify_one( ); } void func2( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( false ); input_data_flag.clear( std::memory_order_release ); data += 2; output_data_flag.test_and_set( std::memory_order_acquire ); output_data_flag.notify_one( ); } int main( ) { std::atomic_flag flag1 { }; std::atomic_flag flag2 { }; int data { 10 }; { std::jthread func1_thread { func1, std::ref( data ), std::ref( flag1 ), std::ref( flag2 ) }; std::jthread func2_thread { func2, std::ref( data ), std::ref( flag2 ), std::ref( flag1 ) }; // here the first thread is finally notified to start its task flag1.test_and_set( std::memory_order_acquire ); flag1.notify_one( ); } return data; }
疑问点
- 哪种方法效率更高?或是二者本质等价(尽管方法1编译后多5行汇编)?
- 方法2是否合理?线程等待flag变为
true后用clear( std::memory_order_release )将其设为false再进入临界区,这似乎有问题。尤其是写入共享资源前使用std::memory_order_release内存序,是否应该换成std::memory_order_acquire?方法2应该使用哪些正确的内存序?
解答
效率与等价性
两种方法本质等价,效率差异可以忽略不计。汇编行数的微小差异来自初始设置flag的操作,但在实际运行中,std::atomic_flag的操作都是原子指令,性能开销几乎一致。两者的核心逻辑都是通过原子flag的状态变化配合wait/notify实现线程间的同步,没有本质的性能优劣之分。
方法2的合理性与内存序修正
方法2的原始写法存在内存序误用,需要修正:
- 线程被唤醒后,在访问共享资源
data之前,需要确保之前的同步操作完成,因此应该使用std::memory_order_acquire语义,而非release。release语义用于确保当前线程的写操作对其他线程可见,而acquire用于确保后续的读/写操作能看到之前的同步状态。 - 方法2中,线程调用
input_data_flag.wait(false)后,正确的操作应该是用test_and_set(std::memory_order_acquire)(而非clear)来标记flag已被处理,避免其他线程误触发;而在通知下一个线程时,应该用clear(std::memory_order_release)来释放同步语义,确保当前线程对data的修改对下一个线程可见。
修正后的方法2代码示例:
#include <atomic> #include <thread> #include <functional> void func1( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( false ); // 用acquire语义确保后续访问data能看到同步状态 input_data_flag.test_and_set( std::memory_order_acquire ); data += 1; // 用release语义确保data的修改对下一个线程可见 output_data_flag.clear( std::memory_order_release ); output_data_flag.notify_one( ); } void func2( int& data, std::atomic_flag& input_data_flag, std::atomic_flag& output_data_flag ) { input_data_flag.wait( false ); input_data_flag.test_and_set( std::memory_order_acquire ); data += 2; output_data_flag.clear( std::memory_order_release ); output_data_flag.notify_one( ); } int main( ) { std::atomic_flag flag1 { }; // 默认初始为false std::atomic_flag flag2 { }; int data { 10 }; { std::jthread func1_thread { func1, std::ref( data ), std::ref( flag1 ), std::ref( flag2 ) }; std::jthread func2_thread { func2, std::ref( data ), std::ref( flag2 ), std::ref( flag1 ) }; // 设置flag1为true,唤醒func1线程,用release语义确保状态变化可见 flag1.test_and_set( std::memory_order_release ); flag1.notify_one( ); } return data; }
修正后,方法2的逻辑就合理了:线程等待flag从初始的false变为true(main调用test_and_set设置),唤醒后用acquire语义建立同步关系,确保后续对data的修改是安全的;通知下一个线程时用release语义,确保当前修改对下一个线程可见。
内容的提问来源于stack exchange,提问作者digito_evo
相关产品推荐
相关产品推荐

