1. 线程同步机制概述
在Linux多线程编程中,线程同步是一个至关重要的概念。想象一下,当多个线程同时访问共享资源时,就像一群人在没有交通信号灯的十字路口同时开车通过,必然会导致混乱和事故。线程同步机制就是为这些"线程交通"建立规则和信号灯,确保数据访问的有序性和安全性。
线程同步的核心目标是解决两类问题:
- 数据竞争:多个线程同时修改同一数据导致结果不可预测
- 执行顺序控制:确保某些操作在其他操作完成后才执行
2. 临界资源与临界区
2.1 基本概念
临界资源是指一次仅允许一个线程使用的共享资源。在多线程环境中,全局变量、静态变量、文件描述符等都是典型的临界资源。
临界区是指访问临界资源的代码段。保护临界区的关键在于确保任何时候只有一个线程在执行临界区代码。
2.2 临界区保护的必要性
考虑一个简单的售票系统示例:
c复制#include <stdio.h>
#include <pthread.h>
int tickets = 100;
void* sell_ticket(void* arg) {
while(tickets > 0) {
usleep(1000); // 模拟售票耗时
tickets--;
printf("剩余票数: %d\n", tickets);
}
return NULL;
}
int main() {
pthread_t t1, t2;
pthread_create(&t1, NULL, sell_ticket, NULL);
pthread_create(&t2, NULL, sell_ticket, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
这个看似简单的程序存在严重问题:
- 可能出现票数为负的情况
- 同一张票可能被多个线程卖出
原因在于tickets--操作并非原子操作,它实际上包含三个步骤:
- 读取tickets值到寄存器
- 寄存器值减1
- 将结果写回内存
3. 互斥锁机制
3.1 互斥锁的基本原理
互斥锁(Mutex)是最常用的线程同步机制,它像厕所的门锁一样:
- 一个线程进入临界区前先"锁门"(加锁)
- 其他线程必须等待直到"门被打开"(解锁)
- 确保任何时候只有一个线程在临界区内
3.2 POSIX线程互斥锁接口
c复制#include <pthread.h>
// 初始化互斥锁
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
// 销毁互斥锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 加锁(阻塞)
int pthread_mutex_lock(pthread_mutex_t *mutex);
// 尝试加锁(非阻塞)
int pthread_mutex_trylock(pthread_mutex_t *mutex);
// 解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
3.3 互斥锁的正确使用
改进后的售票系统:
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* sell_ticket(void* arg) {
while(1) {
pthread_mutex_lock(&mutex);
if(tickets > 0) {
usleep(1000);
tickets--;
printf("剩余票数: %d\n", tickets);
pthread_mutex_unlock(&mutex);
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
return NULL;
}
3.4 RAII风格的互斥锁封装
为避免忘记解锁,可以使用RAII(Resource Acquisition Is Initialization)技术封装互斥锁:
cpp复制class MutexGuard {
public:
explicit MutexGuard(pthread_mutex_t& m) : mutex(m) {
pthread_mutex_lock(&mutex);
}
~MutexGuard() {
pthread_mutex_unlock(&mutex);
}
private:
pthread_mutex_t& mutex;
};
void* sell_ticket(void* arg) {
while(1) {
MutexGuard guard(mutex);
if(tickets > 0) {
usleep(1000);
tickets--;
printf("剩余票数: %d\n", tickets);
} else {
break;
}
}
return NULL;
}
4. 条件变量
4.1 条件变量的作用
条件变量(Condition Variable)用于线程间的条件同步,解决"等待-通知"问题。典型场景:
- 生产者-消费者模型
- 线程池任务调度
- 资源可用性等待
4.2 条件变量接口
c复制#include <pthread.h>
// 初始化条件变量
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
// 销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);
// 等待条件变量
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
// 定时等待
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex,
const struct timespec *abstime);
// 唤醒一个等待线程
int pthread_cond_signal(pthread_cond_t *cond);
// 唤醒所有等待线程
int pthread_cond_broadcast(pthread_cond_t *cond);
4.3 条件变量的正确使用模式
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
bool ready = false;
// 等待线程
void* waiter(void* arg) {
pthread_mutex_lock(&mutex);
while(!ready) { // 必须用while而不是if
pthread_cond_wait(&cond, &mutex);
}
// 处理条件满足后的工作
pthread_mutex_unlock(&mutex);
return NULL;
}
// 通知线程
void* notifier(void* arg) {
pthread_mutex_lock(&mutex);
ready = true;
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mutex);
return NULL;
}
为什么必须用while而不是if检查条件?
- 虚假唤醒:某些实现可能在没有显式唤醒时也会返回
- 多个消费者场景:条件可能在被唤醒后又被其他线程改变
5. 信号量
5.1 信号量的概念
信号量(Semaphore)是一种更通用的同步机制,可以看作是一个计数器,用于控制对多个共享资源的访问。POSIX提供了两种信号量:
- 无名信号量(线程间使用)
- 有名信号量(进程间使用)
5.2 信号量接口
c复制#include <semaphore.h>
// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destroy(sem_t *sem);
// 等待信号量(P操作)
int sem_wait(sem_t *sem);
// 非阻塞等待
int sem_trywait(sem_t *sem);
// 发布信号量(V操作)
int sem_post(sem_t *sem);
// 获取信号量当前值
int sem_getvalue(sem_t *sem, int *sval);
5.3 信号量的应用示例
c复制#define MAX_RESOURCES 5
sem_t available;
void* worker(void* arg) {
sem_wait(&available); // 获取资源
// 使用资源
sem_post(&available); // 释放资源
return NULL;
}
int main() {
sem_init(&available, 0, MAX_RESOURCES);
// 创建多个worker线程
sem_destroy(&available);
return 0;
}
6. 生产者-消费者模型
6.1 模型概述
生产者-消费者模型是多线程编程中的经典问题,包含三个组件:
- 生产者:生成数据并放入缓冲区
- 消费者:从缓冲区取出数据并处理
- 缓冲区:作为中间存储,平衡生产消费速度差异
6.2 基于互斥锁和条件变量的实现
cpp复制#include <queue>
#include <pthread.h>
template<typename T>
class BlockingQueue {
public:
BlockingQueue(size_t capacity) : capacity(capacity) {
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(¬_full, NULL);
pthread_cond_init(¬_empty, NULL);
}
~BlockingQueue() {
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(¬_full);
pthread_cond_destroy(¬_empty);
}
void put(const T& item) {
pthread_mutex_lock(&mutex);
while(queue.size() >= capacity) {
pthread_cond_wait(¬_full, &mutex);
}
queue.push(item);
pthread_cond_signal(¬_empty);
pthread_mutex_unlock(&mutex);
}
T take() {
pthread_mutex_lock(&mutex);
while(queue.empty()) {
pthread_cond_wait(¬_empty, &mutex);
}
T item = queue.front();
queue.pop();
pthread_cond_signal(¬_full);
pthread_mutex_unlock(&mutex);
return item;
}
private:
std::queue<T> queue;
size_t capacity;
pthread_mutex_t mutex;
pthread_cond_t not_full;
pthread_cond_t not_empty;
};
6.3 基于环形缓冲区的实现
环形缓冲区是一种更高效的实现方式,特别适合固定大小的缓冲区:
cpp复制template<typename T>
class RingBuffer {
public:
RingBuffer(size_t size)
: buffer(size), capacity(size), head(0), tail(0) {
sem_init(&empty, 0, capacity);
sem_init(&full, 0, 0);
pthread_mutex_init(&pmutex, NULL);
pthread_mutex_init(&cmutex, NULL);
}
~RingBuffer() {
sem_destroy(&empty);
sem_destroy(&full);
pthread_mutex_destroy(&pmutex);
pthread_mutex_destroy(&cmutex);
}
void put(const T& item) {
sem_wait(&empty); // 等待空位
pthread_mutex_lock(&pmutex);
buffer[tail] = item;
tail = (tail + 1) % capacity;
pthread_mutex_unlock(&pmutex);
sem_post(&full); // 增加可用项
}
T get() {
sem_wait(&full); // 等待可用项
pthread_mutex_lock(&cmutex);
T item = buffer[head];
head = (head + 1) % capacity;
pthread_mutex_unlock(&cmutex);
sem_post(&empty); // 增加空位
return item;
}
private:
std::vector<T> buffer;
size_t capacity;
size_t head, tail;
sem_t empty, full;
pthread_mutex_t pmutex, cmutex;
};
7. 线程同步的常见问题与解决方案
7.1 死锁
死锁是指两个或多个线程互相等待对方持有的资源,导致所有线程都无法继续执行。死锁的四个必要条件:
- 互斥条件
- 占有并等待
- 非抢占条件
- 循环等待
避免死锁的策略:
- 按固定顺序获取锁
- 使用超时机制(pthread_mutex_timedlock)
- 死锁检测算法
- 避免嵌套锁
7.2 优先级反转
优先级反转发生在高优先级线程等待低优先级线程持有的锁,而低优先级线程又被中等优先级线程抢占。
解决方案:
- 优先级继承协议
- 优先级天花板协议
- 使用pthread_mutexattr_setprotocol设置适当的协议
7.3 性能优化
线程同步可能成为性能瓶颈,优化建议:
- 减小临界区范围
- 使用读写锁(pthread_rwlock_t)替代互斥锁
- 考虑无锁数据结构
- 使用线程本地存储(TLS)减少共享数据
8. 实际应用中的经验分享
8.1 锁的粒度选择
- 粗粒度锁:简单但并发性差
- 细粒度锁:复杂但并发性高
- 经验法则:先使用粗粒度锁,发现性能瓶颈后再考虑细化
8.2 条件变量的使用技巧
- 总是与互斥锁配合使用
- 检查条件必须使用while循环
- 在改变条件后及时发出信号
- 考虑使用pthread_cond_broadcast而非pthread_cond_signal
8.3 调试多线程程序
- 使用valgrind --tool=helgrind检测数据竞争
- 使用gdb的thread命令查看线程状态
- 添加详细的日志记录
- 逐步增加线程数量测试
9. 现代C++中的线程同步
C++11引入了
cpp复制#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>
std::mutex mtx;
std::condition_variable cv;
std::queue<int> msgQueue;
void producer() {
for(int i = 0; i < 10; ++i) {
std::this_thread::sleep_for(std::chrono::milliseconds(100));
{
std::lock_guard<std::mutex> lock(mtx);
msgQueue.push(i);
cv.notify_one();
}
}
}
void consumer() {
while(true) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return !msgQueue.empty(); });
int msg = msgQueue.front();
msgQueue.pop();
lock.unlock();
std::cout << "Received: " << msg << std::endl;
if(msg == 9) break;
}
}
int main() {
std::thread t1(producer);
std::thread t2(consumer);
t1.join();
t2.join();
return 0;
}
C++标准库提供的同步工具包括:
- std::mutex
- std::lock_guard
- std::unique_lock
- std::condition_variable
- std::atomic
- std::future/std::promise
10. 线程同步的最佳实践
- 最小化共享数据:尽可能减少需要同步的数据
- 优先使用高级抽象:如C++标准库或第三方线程安全容器
- 避免过度同步:不必要的同步会降低性能
- 测试多线程场景:特别注意边界条件和竞态条件
- 文档化线程安全保证:明确哪些操作是线程安全的
在实际项目中,线程同步的选择应该基于:
- 性能需求
- 代码复杂度
- 可维护性
- 团队熟悉度
记住:没有放之四海而皆准的同步方案,只有最适合当前场景的选择。
