1. 线程同步的本质与必要性
当多个执行流同时访问共享资源时,如果没有合理的协调机制,就会出现数据竞争(Data Race)问题。我曾在电商秒杀系统开发中亲眼目睹过库存计数错误:明明只卖了1000件商品,数据库却显示减少了1200件库存。这种诡异现象的根源就在于多个线程同时执行"库存减1"操作时发生了交叉执行。
线程同步的核心思想就像十字路口的红绿灯——通过特定的规则让不同方向的车辆(线程)有序通过临界区(路口)。在编程中,临界区指的是访问共享资源的代码段。以下是典型的竞态条件示例:
cpp复制// 共享变量
int counter = 0;
void increment() {
for (int i = 0; i < 100000; ++i) {
counter++; // 这不是原子操作!
}
}
当两个线程同时执行increment()时,由于counter++实际上包含"读取-修改-写入"三个步骤,最终结果往往会小于预期的200000。我在性能测试中发现,这种错误发生的概率随着线程数增加呈指数级上升。
关键认知:现代CPU的乱序执行和缓存一致性协议(如MESI)会使多线程问题更加隐蔽。有时候即使代码"看起来"顺序正确,实际执行时仍可能出错。
2. 互斥锁的深度实践
2.1 互斥锁的实现原理
互斥锁(Mutex)是最基础的同步原语,其核心是一个二进制标志位和等待队列。以Linux的pthread_mutex_t为例,其工作流程如下:
- 线程尝试获取锁时,CPU会执行原子性的CAS(Compare-And-Swap)操作
- 成功则进入临界区,失败则被放入等待队列并触发上下文切换
- 锁释放时唤醒等待队列中的线程
C++11后的标准库提供了更易用的std::mutex:
cpp复制std::mutex mtx;
int shared_data = 0;
void safe_increment() {
for (int i = 0; i < 100000; ++i) {
mtx.lock();
shared_data++;
mtx.unlock();
}
}
2.2 锁的粒度控制实战
锁粒度是影响性能的关键因素。在开发高并发交易系统时,我通过以下优化将吞吐量提升了3倍:
- 粗粒度锁:保护整个数据结构
cpp复制std::mutex db_mutex;
void process_transaction() {
db_mutex.lock();
// 执行所有数据库操作
db_mutex.unlock();
}
- 细粒度锁:只保护必要部分
cpp复制std::mutex account_mutex[ACCOUNT_NUM];
void transfer(int from, int to) {
std::lock_guard<std::mutex> lock1(account_mutex[from]);
std::lock_guard<std::mutex> lock2(account_mutex[to]);
// 只锁定涉及的两个账户
}
血泪教训:过度细化的锁可能导致死锁。我曾遇到过一个订单系统因为锁顺序不一致导致的死锁,最终采用std::lock()同时锁定多个互斥量才解决。
3. 条件变量的精妙运用
3.1 生产者-消费者模型实现
条件变量(Condition Variable)适合解决"等待特定条件成立"的场景。下面是高效的内存缓存实现:
cpp复制std::mutex mtx;
std::condition_variable cv;
std::queue<Data> cache;
const int MAX_SIZE = 100;
void producer() {
while (true) {
Data data = generate_data();
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return cache.size() < MAX_SIZE; });
cache.push(data);
}
cv.notify_one();
}
}
void consumer() {
while (true) {
Data data;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !cache.empty(); });
data = cache.front();
cache.pop();
}
cv.notify_one();
process(data);
}
}
3.2 虚假唤醒的防御策略
条件变量的wait()必须放在while循环中检查条件,这是我在线上事故中学到的教训。某些系统实现可能导致虚假唤醒(spurious wakeup),正确的写法:
cpp复制cv.wait(lock, [&]{ return !queue.empty(); });
// 等价于
while (queue.empty()) {
cv.wait(lock);
}
4. 原子操作的底层揭秘
4.1 内存顺序详解
C++11提供了六种内存顺序(memory_order),理解它们对编写高性能代码至关重要:
- memory_order_relaxed:只保证原子性,不保证顺序
- memory_order_consume:依赖该原子变量的后续操作不乱序
- memory_order_acquire:后续读操作不能重排到前面
- memory_order_release:前面写操作不能重排到后面
- memory_order_acq_rel:acquire + release
- memory_order_seq_cst:完全顺序一致性(默认)
cpp复制std::atomic<bool> ready{false};
int data = 0;
void producer() {
data = 42; // 1
ready.store(true, std::memory_order_release); // 2
}
void consumer() {
while (!ready.load(std::memory_order_acquire)); // 3
assert(data == 42); // 4
}
4.2 CAS操作的妙用
比较交换(Compare-And-Swap)是原子操作的核心,可实现无锁数据结构:
cpp复制template<typename T>
class lock_free_stack {
struct node {
T data;
node* next;
};
std::atomic<node*> head;
public:
void push(const T& data) {
node* new_node = new node{data, nullptr};
new_node->next = head.load();
while (!head.compare_exchange_weak(new_node->next, new_node));
}
};
我在实现连接池时发现,CAS在低竞争场景下比互斥锁快10倍,但在高竞争时反而更慢——这就是著名的"乐观锁"与"悲观锁"的取舍问题。
5. 死锁预防实战手册
5.1 死锁的四个必要条件
- 互斥条件:资源一次只能被一个线程占有
- 占有且等待:持有资源的同时等待其他资源
- 不可抢占:资源只能由持有者释放
- 循环等待:存在线程资源的环形等待链
5.2 银行家算法实践
在资源分配系统中,我采用类似银行家算法的策略:
cpp复制bool is_safe(const std::vector<int>& available,
const std::vector<std::vector<int>>& max,
const std::vector<std::vector<int>>& allocation) {
auto need = calculate_need(max, allocation);
std::vector<bool> finish(thread_count, false);
auto work = available;
while (true) {
bool found = false;
for (int i = 0; i < thread_count; ++i) {
if (!finish[i] && need[i] <= work) {
work += allocation[i];
finish[i] = true;
found = true;
}
}
if (!found) break;
}
return std::all_of(finish.begin(), finish.end(), [](bool b){ return b; });
}
5.3 锁排序技巧
所有线程按固定顺序获取锁是预防死锁的有效方法。我在数据库中间件中这样实现:
cpp复制void transaction(Account& a, Account& b) {
auto lock1 = std::unique_lock<std::mutex>(
a.id < b.id ? a.mtx : b.mtx);
auto lock2 = std::unique_lock<std::mutex>(
a.id < b.id ? b.mtx : a.mtx, std::defer_lock);
std::lock(lock1, lock2);
// 安全操作...
}
6. 性能优化关键指标
6.1 锁竞争度量方法
使用perf工具监控锁竞争:
bash复制perf stat -e L1-dcache-load-misses,LLC-load-misses,cycles,instructions,cache-references,cache-misses ./program
6.2 自适应自旋锁实现
结合互斥锁和自旋锁的优点:
cpp复制class hybrid_mutex {
std::atomic<bool> locked{false};
static const int spin_count = 1000;
public:
void lock() {
for (int i = 0; i < spin_count; ++i) {
if (!locked.exchange(true)) return;
std::this_thread::yield();
}
while (locked.exchange(true));
}
void unlock() { locked.store(false); }
};
在8核服务器上测试显示,该实现在低竞争时接近自旋锁性能,高竞争时退化为互斥锁行为。