文章目录
5.3 同步操作与强制排序
在多线程编程中,当多个线程同时访问共享数据时,需要谨慎处理同步问题。让我们通过一个简单例子来理解这个概念:假设一个线程向数据结构写入数据,另一个线程从中读取数据。为了避免数据竞争,写入线程会设置一个标志位表示数据已准备就绪,读取线程则需等待该标志位被设置后才能读取数据。
代码5.2 多线程数据读写示例
#include <vector>
#include <atomic>
#include <iostream>
#include <thread>
std::vector<int> data;
std::atomic<bool> data_ready(false);
void reader_thread()
{
while(!data_ready.load()) // 1. 等待数据准备就绪
{
std::this_thread::sleep(std::chrono::milliseconds(1));
}
std::cout << "The answer=" << data[0] << "\n"; // 2. 读取数据
}
void writer_thread()
{
data.push_back(42); // 3. 写入数据
data_ready = true; // 4. 设置数据就绪标志
}
int main()
{
std::thread writer(writer_thread);
std::thread reader(reader_thread);
writer.join();
reader.join();
return 0;
}
在这个例子中,虽然等待循环①本身是原子的,但非原子读取操作②和写入操作③如果无序执行,就会产生未定义行为。我们通过原子变量data_ready
的操作来建立执行顺序:数据写入③必须先于标志设置④,标志检查①必须先于数据读取②。当data_ready
为true时,写操作与读操作同步,建立了"先行"关系。
5.3.1 同步发生
"同步发生"关系只在原子类型操作间存在。当线程A执行原子写操作,线程B执行原子读操作且读取的是A写入的值(或之后写入的值),那么A的写操作与B的读操作就是同步发生关系。
5.3.2 先行发生
"先行发生"关系是程序操作顺序的基本构建块。在单线程中,如果操作A在操作B之前执行,那么A就先行于B。在多线程环境中,如果操作A与另一线程上的操作B同步发生,那么A线程间先行于B。
5.3.3 原子操作的内存序
C++提供了六种内存序选项:
memory_order_relaxed
- 自由序memory_order_consume
- 消费序(C++17中不推荐使用)memory_order_acquire
- 获取序memory_order_release
- 释放序memory_order_acq_rel
- 获取-释放序memory_order_seq_cst
- 顺序一致性序(默认)
顺序一致性序
顺序一致性是最严格的内存序,保证所有线程看到的操作顺序一致。下面是顺序一致性的示例:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x() { x.store(true, std::memory_order_seq_cst); }
void write_y() { y.store(true, std::memory_order_seq_cst); }
void read_x_then_y()
{
while(!x.load(std::memory_order_seq_cst));
if(y.load(std::memory_order_seq_cst)) ++z;
}
void read_y_then_x()
{
while(!y.load(std::memory_order_seq_cst));
if(x.load(std::memory_order_seq_cst)) ++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x);
std::thread b(write_y);
std::thread c(read_x_then_y);
std::thread d(read_y_then_x);
a.join();
b.join();
c.join();
d.join();
assert(z.load() != 0); // 永远不会触发
}
自由序
自由序只保证原子操作的原子性,不提供任何顺序保证:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed); // 1
y.store(true, std::memory_order_relaxed); // 2
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed)); // 3
if(x.load(std::memory_order_relaxed)) ++z; // 4
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 可能触发!
}
获取-释放序
获取-释放序提供了比自由序更强的同步保证:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release); // 释放栅栏
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire); // 获取栅栏
if(x.load(std::memory_order_relaxed)) ++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 不会触发
}
5.3.4 释放序列与同步
释放序列确保了一系列原子操作的正确同步:
#include <atomic>
#include <thread>
#include <vector>
std::vector<int> queue_data;
std::atomic<int> count;
void populate_queue()
{
unsigned const number_of_items = 20;
queue_data.clear();
for(unsigned i = 0; i < number_of_items; ++i)
{
queue_data.push_back(i);
}
count.store(number_of_items, std::memory_order_release);
}
void process(int item) { /* 处理数据 */ }
void consume_queue_items()
{
while(true)
{
int item_index;
if((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0)
{
continue; // 等待更多项目
}
process(queue_data[item_index - 1]);
}
}
int main()
{
std::thread a(populate_queue);
std::thread b(consume_queue_items);
std::thread c(consume_queue_items);
a.join();
b.join();
c.join();
}
5.3.5 栅栏
内存栅栏提供了对内存操作顺序的强制约束:
#include <atomic>
#include <thread>
#include <assert.h>
std::atomic<bool> x, y;
std::atomic<int> z;
void write_x_then_y()
{
x.store(true, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
if(x.load(std::memory_order_relaxed)) ++z;
}
int main()
{
x = false;
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 不会触发
}
5.3.6 原子操作对非原子操作排序
原子操作也可以对非原子操作进行排序:
#include <atomic>
#include <thread>
#include <assert.h>
bool x = false; // 非原子变量
std::atomic<bool> y;
std::atomic<int> z;
void write_x_then_y()
{
x = true; // 非原子写入
std::atomic_thread_fence(std::memory_order_release);
y.store(true, std::memory_order_relaxed);
}
void read_y_then_x()
{
while(!y.load(std::memory_order_relaxed));
std::atomic_thread_fence(std::memory_order_acquire);
if(x) ++z; // 读取非原子变量
}
int main()
{
y = false;
z = 0;
std::thread a(write_x_then_y);
std::thread b(read_y_then_x);
a.join();
b.join();
assert(z.load() != 0); // 不会触发
}
5.3.7 非原子操作排序
非原子操作可以通过原子操作进行排序,这是更高级同步工具的基础:
#include <atomic>
#include <thread>
class spinlock_mutex
{
std::atomic_flag flag;
public:
spinlock_mutex() : flag(ATOMIC_FLAG_INIT) {}
void lock()
{
while(flag.test_and_set(std::memory_order_acquire));
}
void unlock()
{
flag.clear(std::memory_order_release);
}
};
spinlock_mutex mutex;
int shared_data = 0;
void worker()
{
mutex.lock();
++shared_data; // 受保护的操作
mutex.unlock();
}
int main()
{
std::thread t1(worker);
std::thread t2(worker);
t1.join();
t2.join();
return 0;
}
C++标准库提供了多种同步机制,包括互斥量、条件变量、future等,它们都基于这些基本的内存序概念构建,为多线程编程提供了更高级的抽象。
理解这些内存序概念对于编写正确高效的多线程程序至关重要。在实际开发中,应该优先使用高级同步工具,只有在需要极致性能时才考虑直接使用原子操作和内存序。