多线程编程:原子操作、内存顺序与性能优化
立即解锁
发布时间: 2025-08-20 01:47:52 阅读量: 2 订阅数: 3 


C++高性能编程:从入门到精通
### 多线程编程:原子操作、内存顺序与性能优化
#### 1. 原子操作与内存顺序
在单线程的 C++ 程序中,不会出现数据竞争的风险,我们可以愉快地编写程序而无需考虑指令重排。但在多线程程序中,涉及共享变量时情况就完全不同了。编译器(和硬件)的优化是基于单线程的可见性进行的,它无法知晓其他线程通过共享变量能观察到什么,因此我们程序员有责任告知编译器哪些重排是允许的。使用原子变量或互斥锁来避免数据竞争,就是在做这件事。
当使用互斥锁保护临界区时,只有持有锁的线程才能执行临界区代码。同时,互斥锁会在临界区周围创建内存屏障,告知系统在临界区边界某些重排是不允许的。获取锁时会添加获取屏障,释放锁时会添加释放屏障。
例如,假设有四条指令 i1、i2、i3 和 i4,它们相互独立,系统可以任意重排这些指令而不产生可观察的影响。但 i2 和 i3 使用了共享数据,需要用互斥锁保护。添加互斥锁的获取和释放操作后,某些重排就不再有效了。临界区内的指令不能移出临界区,否则就无法得到互斥锁的保护。单向屏障确保指令不能从临界区移出。i1 指令可以穿过获取屏障进入临界区,但不能越过释放屏障;i4 指令可以穿过释放屏障进入临界区,但不能越过获取屏障。
获取互斥锁时会创建获取内存屏障,它告诉系统,任何内存访问(读或写)都不能移动到获取屏障所在的行之上。系统可以将 i4 指令移到释放屏障之上、i3 和 i2 指令之后,但由于获取屏障的存在,不能再继续移动。
使用共享原子变量有两个好处:
- 防止撕裂写入:原子变量总是原子更新的,读者不会读到部分写入的值。
- 通过添加足够的内存屏障来同步内存:这可以防止某些指令重排,保证原子操作指定的内存顺序。
如果程序没有数据竞争,并且在使用原子操作时使用默认内存顺序,C++ 内存模型保证顺序一致性。顺序一致性保证程序的执行结果与按原程序指定的顺序执行操作的结果相同。线程间指令的交错是任意的,我们无法控制线程的调度。不过,顺序一致性可能会影响性能,因此也可以使用具有宽松内存模型的原子操作,但除非你非常清楚宽松内存模型的影响,否则建议使用默认的顺序一致性内存顺序。
#### 2. 无锁编程
无锁编程是一项具有挑战性的任务。这里给出一个简单的无锁队列的实现示例。无锁队列是一种相对简单但实用的无锁数据结构,可用于与不能使用锁来同步共享数据访问的线程进行单向通信。
这个无锁队列的实现有一些限制:它只支持一个读者线程和一个写者线程,并且队列的容量是固定的,运行时不能改变。
写者线程可以调用:
- `push()`:向队列中添加一个元素。
读者线程可以调用:
- `front()`:返回队列的队首元素。
- `pop()`:移除队列的队首元素。
两个线程都可以调用:
- `size()`:返回队列的当前大小。
以下是队列的完整实现:
```cpp
template <class T, size_t N>
class LockFreeQueue {
public:
LockFreeQueue() : read_pos_{0}, write_pos_{0}, size_{0} {
assert(size_.is_lock_free());
}
auto size() const { return size_.load(); }
// Writer thread
auto push(const T& t) {
if (size_.load() >= N) {
throw std::overflow_error("Queue is full");
}
buffer_[write_pos_] = t;
write_pos_ = (write_pos_ + 1) % N;
size_.fetch_add(1);
}
// Reader thread
auto& front() const {
auto s = size_.load();
if (s == 0) {
throw std::underflow_error("Queue is empty");
}
return buffer_[read_pos_];
}
// Reader thread
auto pop() {
if (size_.load() == 0) {
throw std::underflow_error("Queue is empty");
}
read_pos_ = (read_pos_ + 1) % N;
size_.fetch_sub(1);
}
private:
std::array<T, N> buffer_{}; // Used by both threads
std::atomic<size_t> size_{}; // Used by both threads
size_t read_pos_ = 0; // Used by reader thread
size_t write_pos_ = 0; // Used by writer thread
};
```
唯一需要原子访问的数据成员是 `size_` 变量。`read_pos_` 仅由读者线程使用,`write_pos_` 仅由写者线程使用。对于 `std::array` 类型的 `buffer`,虽然它是可变的且被两个线程访问,但由于算法确保两个线程不会同时访问数组中的同一个元素,C++ 保证可以无数据竞争地访问数组中的单个元素,即使是字符数组也有此保证。
这种无阻塞队列在音频编程中很有用。例如,主线程上运行的 UI 需要与实时音频线程发送或接收数据,而实时音频线程在任何情况下都不能阻塞,它不能使用互斥锁、分配/释放内存或执行任何可能导致线程等待低优先级线程的操作,这时就需要无锁数据结构。
在 `LockFreeQueue` 中,读者
0
0
复制全文
相关推荐










