在多线程编程中,线程间的并发访问常常导致数据错乱、死锁等问题。线程同步与互斥是解决这些问题的核心技术,它们确保多个线程安全地共享资源,同时按预期顺序执行。本文将从线程互斥的基本概念出发,详细讲解互斥量、条件变量、信号量等同步机制,结合生产者 - 消费者模型和线程池实战,帮助你掌握多线程编程的关键技能。
一、线程互斥:解决共享资源竞争问题
当多个线程并发访问临界资源(如全局变量、文件描述符)时,若缺乏保护机制,会导致数据一致性问题。线程互斥的核心是保证 “任意时刻只有一个线程进入临界区”,实现这一目标的核心工具是互斥量(Mutex)。
1.1 核心概念辨析
在学习互斥量前,需明确以下基础概念:
临界资源:多线程共享的资源(如全局变量、硬件设备),需通过互斥保护。
临界区:线程中访问临界资源的代码段(需加锁保护)。
互斥:确保同一时间只有一个线程进入临界区,避免资源竞争。
原子性:操作不可被打断,要么完全执行,要么不执行(如swap指令)。
1.2 为什么需要互斥?—— 一个反例
以 “多线程售票系统” 为例,多个线程并发执行售票逻辑时,会出现超卖或卖负票的问题:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
int ticket = 100; // 临界资源:剩余票数
void *sell_ticket(void *arg) {
char *thread_id = (char *)arg;
while (1) {
if (ticket > 0) {
usleep(1000); // 模拟售票业务耗时
printf("%s 售出车票:%d\n", thread_id, ticket);
ticket--; // 非原子操作:load→update→store
} else {
break;
}
}
return NULL;
}
int main() {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, sell_ticket, "线程1");
pthread_create(&t2, NULL, sell_ticket, "线程2");
pthread_create(&t3, NULL, sell_ticket, "线程3");
pthread_create(&t4, NULL, sell_ticket, "线程4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
运行结果(错误示例):
线程4 售出车票:100
线程2 售出车票:0
线程1 售出车票:-1
线程3 售出车票:-2
问题原因:
ticket--并非原子操作,对应三条汇编指令:
load:将ticket从内存加载到寄存器。
update:寄存器值减 1。
store:将新值写回内存。
线程切换可能发生在任意指令之间,导致多个线程读取到相同的ticket值, 最终出现超卖。
1.3 互斥量(Mutex):实现线程互斥
互斥量是 Linux 提供的同步工具,通过 “加锁 - 访问 - 解锁” 的流程,确保临界区的原子性访问。
1.3.1 互斥量的核心接口
POSIX 线程库提供了互斥量的完整操作接口,所有函数以pthread_mutex_开头,需链接-lpthread库:
| 函数 | 功能 | 关键参数 / 返回值 |
|---|---|---|
pthread_mutex_init | 初始化互斥量 | attr=NULL表示默认属性,成功返回 0 |
pthread_mutex_destroy | 销毁互斥量 | 不可销毁已加锁的互斥量 |
pthread_mutex_lock | 加锁 | 若互斥量已被锁定,调用线程阻塞 |
pthread_mutex_unlock | 解锁 | 仅持有锁的线程可调用,成功返回 0 |
1.3.2 用互斥量修复售票系统
在临界区前后加锁和解锁,确保ticket的操作原子性:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
int ticket = 100;
pthread_mutex_t mutex; // 定义互斥量
void *sell_ticket(void *arg) {
char *thread_id = (char *)arg;
while (1) {
pthread_mutex_lock(&mutex); // 加锁:进入临界区
if (ticket > 0) {
usleep(1000);
printf("%s 售出车票:%d\n", thread_id, ticket);
ticket--;
pthread_mutex_unlock(&mutex); // 解锁:离开临界区
} else {
pthread_mutex_unlock(&mutex); // 解锁:避免死锁
break;
}
}
return NULL;
}
int main() {
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL); // 初始化互斥量
pthread_create(&t1, NULL, sell_ticket, "线程1");
pthread_create(&t2, NULL, sell_ticket, "线程2");
pthread_create(&t3, NULL, sell_ticket, "线程3");
pthread_create(&t4, NULL, sell_ticket, "线程4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex); // 销毁互斥量
return 0;
}
运行结果(正确示例):
线程1 售出车票:100
线程2 售出车票:99
线程3 售出车票:98
...
线程4 售出车票:1
1.3.3 互斥量的实现原理
互斥量的原子性依赖硬件提供的swap或exchange指令,该指令可原子地交换寄存器和内存的数据。简化的实现逻辑如下:
加锁(lock):
用swap指令将互斥量的值(内存)与寄存器的 0 交换。
若交换后寄存器值为 1(互斥量未锁定),加锁成功。
若寄存器值为 0(互斥量已锁定),线程阻塞等待。
解锁(unlock):
将互斥量的值设为 1(内存)。
唤醒等待该互斥量的线程。
1.4 互斥量的封装:RAII 风格
直接使用pthread_mutex_接口容易出现 “忘记解锁” 或 “销毁已加锁的互斥量” 等问题。采用RAII(资源获取即初始化) 风格封装互斥量,可自动管理锁的生命周期:
// Lock.hpp
#pragma once
#include <pthread.h>
namespace LockModule {
// 互斥量封装
class Mutex {
public:
Mutex() { pthread_mutex_init(&_mutex, NULL); }
~Mutex() { pthread_mutex_destroy(&_mutex); }
// 禁止拷贝(互斥量不可复制)
Mutex(const Mutex&) = delete;
Mutex& operator=(const Mutex&) = delete;
void Lock() { pthread_mutex_lock(&_mutex); }
void Unlock() { pthread_mutex_unlock(&_mutex); }
pthread_mutex_t* GetRawMutex() { return &_mutex; }
private:
pthread_mutex_t _mutex;
};
// RAII风格的锁守卫:构造加锁,析构解锁
class LockGuard {
public:
explicit LockGuard(Mutex& mutex) : _mutex(mutex) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
// 禁止拷贝
LockGuard(const LockGuard&) = delete;
LockGuard& operator=(const LockGuard&) = delete;
private:
Mutex& _mutex;
};
} // namespace LockModule
使用示例:
#include "Lock.hpp"
using namespace LockModule;
int ticket = 100;
Mutex mutex;
void *sell_ticket(void *arg) {
char *thread_id = (char *)arg;
while (1) {
LockGuard lock(mutex); // 构造加锁,析构自动解锁
if (ticket > 0) {
usleep(1000);
printf("%s 售出车票:%d\n", thread_id, ticket);
ticket--;
} else {
break;
}
}
return NULL;
}
二、线程同步:控制线程执行顺序
互斥解决了 “资源竞争” 问题,但未解决 “执行顺序” 问题。例如:消费者线程需等待生产者线程生产数据后才能消费,此时需通过条件变量(Condition Variable) 实现线程同步。
2.1 条件变量的核心概念
同步:在保证数据安全的前提下,让线程按特定顺序访问临界资源(如 “生产者先生产,消费者后消费”)。
条件变量:用于线程间的 “等待 - 通知” 机制,线程可等待某个条件满足,或通知其他线程条件已满足。
2.2 条件变量的核心接口
POSIX 线程库的条件变量接口与互斥量配合使用,确保条件判断的原子性:
| 函数 | 功能 | 关键参数 |
|---|---|---|
pthread_cond_init | 初始化条件变量 | attr=NULL表示默认属性 |
pthread_cond_destroy | 销毁条件变量 | 不可销毁有线程等待的条件变量 |
pthread_cond_wait | 等待条件满足 | 需传入互斥量,自动释放锁并阻塞 |
pthread_cond_signal | 唤醒一个等待线程 | 随机唤醒一个等待的线程 |
pthread_cond_broadcast | 唤醒所有等待线程 | 唤醒所有等待的线程 |
关键细节:为什么pthread_cond_wait需要互斥量?
条件判断(如 “队列是否为空”)依赖共享资源(如队列),需互斥量保护。
pthread_cond_wait会原子地执行 “释放互斥量 + 阻塞线程”,避免 “解锁后、等待前” 的信号丢失问题。
线程被唤醒后,会自动重新获取互斥量,确保后续操作的安全性。
2.3 生产者 - 消费者模型:条件变量的经典应用
生产者 - 消费者模型是多线程同步的经典场景,通过 “阻塞队列” 解耦生产者和消费者,平衡两者的处理能力(遵循 “321 原则”:3 种关系、2 个角色、1 个容器)。
2.3.1 基于阻塞队列的实现
阻塞队列(Blocking Queue)的核心特性:
队列为空时,消费者线程阻塞等待。
队列满时,生产者线程阻塞等待。
生产者生产数据后,通知消费者;消费者消费后,通知生产者。
// BlockQueue.hpp
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>
#include "Lock.hpp" // 引入RAII风格的互斥量
namespace SyncModule {
using namespace LockModule;
template <typename T>
class BlockQueue {
public:
explicit BlockQueue(int cap) : _capacity(cap) {
pthread_cond_init(&_prod_cond, NULL); // 生产者条件变量
pthread_cond_init(&_cons_cond, NULL); // 消费者条件变量
}
~BlockQueue() {
pthread_cond_destroy(&_prod_cond);
pthread_cond_destroy(&_cons_cond);
}
// 生产者入队
void Enqueue(const T& data) {
LockGuard lock(_mutex); // 加锁
// 队列满时,生产者等待
while (IsFull()) { // 用while避免“伪唤醒”
pthread_cond_wait(&_prod_cond, _mutex.GetRawMutex());
}
// 生产数据
_queue.push(data);
// 通知消费者:有数据可消费
pthread_cond_signal(&_cons_cond);
}
// 消费者出队
void Dequeue(T& data) {
LockGuard lock(_mutex); // 加锁
// 队列空时,消费者等待
while (IsEmpty()) { // 用while避免“伪唤醒”
pthread_cond_wait(&_cons_cond, _mutex.GetRawMutex());
}
// 消费数据
data = _queue.front();
_queue.pop();
// 通知生产者:有空间可生产
pthread_cond_signal(&_prod_cond);
}
private:
bool IsFull() const { return _queue.size() == _capacity; }
bool IsEmpty() const { return _queue.empty(); }
private:
std::queue<T> _queue; // 阻塞队列
int _capacity; // 队列容量上限
Mutex _mutex; // 保护队列的互斥量
pthread_cond_t _prod_cond; // 生产者条件变量
pthread_cond_t _cons_cond; // 消费者条件变量
};
} // namespace SyncModule
2.3.2 生产者 - 消费者模型测试
#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include "BlockQueue.hpp"
using namespace SyncModule;
using namespace std;
// 生产者线程:生成1~100的整数
void* producer(void* arg) {
BlockQueue<int>* bq = (BlockQueue<int>*)arg;
for (int i = 1; i <= 100; ++i) {
bq->Enqueue(i);
printf("生产者[%lu]:生产数据%d\n", pthread_self(), i);
usleep(50000); // 模拟生产耗时
}
return NULL;
}
// 消费者线程:消费整数并打印
void* consumer(void* arg) {
BlockQueue<int>* bq = (BlockQueue<int>*)arg;
int data;
while (true) {
bq->Dequeue(data);
printf("消费者[%lu]:消费数据%d\n", pthread_self(), data);
if (data == 100) break; // 消费完最后一个数据后退出
usleep(100000); // 模拟消费耗时
}
return NULL;
}
int main() {
BlockQueue<int> bq(5); // 队列容量为5
pthread_t prod_tid, cons_tid;
pthread_create(&prod_tid, NULL, producer, &bq);
pthread_create(&cons_tid, NULL, consumer, &bq);
pthread_join(prod_tid, NULL);
pthread_join(cons_tid, NULL);
return 0;
}
运行结果:
生产者[140703376586496]:生产数据1
消费者[140703368193808]:消费数据1
生产者[140703376586496]:生产数据2
生产者[140703376586496]:生产数据3
生产者[140703376586496]:生产数据4
生产者[140703376586496]:生产数据5
生产者[140703376586496]:生产数据6 // 队列满,生产者阻塞
消费者[140703368193808]:消费数据2
生产者[140703376586496]:生产数据6 // 消费者消费后,生产者被唤醒
...
三、POSIX 信号量:另一种同步机制
信号量是比互斥量更通用的同步工具,可用于线程间或进程间的同步,支持 “多资源并发访问”(如允许 3 个线程同时访问某个资源)。
3.1 信号量的核心概念
信号量值:表示可用资源的数量,sem > 0表示有资源可用,sem = 0表示无资源可用。
P 操作:sem_wait,信号量值减 1,若sem < 0则线程阻塞。
V 操作:sem_post,信号量值加 1,若sem <= 0则唤醒一个阻塞线程。
3.2 信号量的核心接口
#include <semaphore.h>
// 初始化信号量:pshared=0表示线程间共享,value为初始值
int sem_init(sem_t* sem, int pshared, unsigned int value);
// 销毁信号量
int sem_destroy(sem_t* sem);
// P操作:申请资源,sem--
int sem_wait(sem_t* sem);
// V操作:释放资源,sem++
int sem_post(sem_t* sem);
3.3 基于环形队列的生产者 - 消费者模型
用信号量实现固定大小的环形队列,支持多生产者和多消费者:
// RingQueue.hpp
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>
template <typename T>
class RingQueue {
public:
explicit RingQueue(int cap) : _capacity(cap), _prod_idx(0), _cons_idx(0) {
_queue.resize(cap);
// 初始化信号量:_empty表示空槽数量(初始为cap),_full表示数据数量(初始为0)
sem_init(&_empty, 0, cap);
sem_init(&_full, 0, 0);
// 初始化互斥量:保护生产者/消费者的索引操作
pthread_mutex_init(&_prod_mutex, NULL);
pthread_mutex_init(&_cons_mutex, NULL);
}
~RingQueue() {
sem_destroy(&_empty);
sem_destroy(&_full);
pthread_mutex_destroy(&_prod_mutex);
pthread_mutex_destroy(&_cons_mutex);
}
// 生产者入队
void Enqueue(const T& data) {
sem_wait(&_empty); // P操作:申请空槽
pthread_mutex_lock(&_prod_mutex); // 保护生产者索引
_queue[_prod_idx] = data;
_prod_idx = (_prod_idx + 1) % _capacity; // 环形索引
pthread_mutex_unlock(&_prod_mutex);
sem_post(&_full); // V操作:增加数据数量
}
// 消费者出队
void Dequeue(T& data) {
sem_wait(&_full); // P操作:申请数据
pthread_mutex_lock(&_cons_mutex); // 保护消费者索引
data = _queue[_cons_idx];
_cons_idx = (_cons_idx + 1) % _capacity; // 环形索引
pthread_mutex_unlock(&_cons_mutex);
sem_post(&_empty); // V操作:增加空槽数量
}
private:
std::vector<T> _queue; // 环形队列
int _capacity; // 队列容量
int _prod_idx; // 生产者索引
int _cons_idx; // 消费者索引
sem_t _empty; // 空槽信号量
sem_t _full; // 数据信号量
pthread_mutex_t _prod_mutex;// 生产者互斥量
pthread_mutex_t _cons_mutex;// 消费者互斥量
};
四、线程池:多线程的工程化应用
线程池是多线程编程的常用组件,通过预先创建固定数量的线程,复用线程处理任务,避免频繁创建 / 销毁线程的开销。
4.1 线程池的核心设计
线程池的核心组成:
线程队列:预先创建的线程,等待处理任务。
任务队列:存储待处理的任务(如函数对象)。
同步机制:用互斥量保护任务队列,用条件变量通知线程有任务到达。
4.2 线程池的实现(单例模式)
为确保进程中只有一个线程池实例,采用懒汉式单例模式(线程安全版),结合之前封装的互斥量和条件变量:
// ThreadPool.hpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <functional>
#include <pthread.h>
#include "Lock.hpp"
#include "Cond.hpp" // 条件变量封装(类似Mutex封装)
using namespace LockModule;
using namespace CondModule;
// 任务类型:无参无返回值的函数对象
using Task = std::function<void()>;
class ThreadPool {
public:
// 单例模式:获取线程池实例
static ThreadPool* GetInstance(int thread_num = 10) {
if (_instance == nullptr) { // 双重检查,减少锁竞争
LockGuard lock(_singleton_mutex);
if (_instance == nullptr) {
_instance = new ThreadPool(thread_num);
}
}
return _instance;
}
// 禁止拷贝
ThreadPool(const ThreadPool&) = delete;
ThreadPool& operator=(const ThreadPool&) = delete;
// 提交任务到任务队列
void SubmitTask(const Task& task) {
LockGuard lock(_task_mutex);
_task_queue.push(task);
_task_cond.Notify(); // 通知线程有任务到达
}
// 停止线程池
void Stop() {
LockGuard lock(_task_mutex);
_is_running = false;
_task_cond.NotifyAll(); // 唤醒所有等待的线程
}
// 等待所有线程退出
void Wait() {
for (pthread_t tid : _threads) {
pthread_join(tid, NULL);
}
}
private:
// 私有构造函数:初始化线程池
explicit ThreadPool(int thread_num) : _thread_num(thread_num), _is_running(true) {
// 创建线程
for (int i = 0; i < thread_num; ++i) {
pthread_t tid;
pthread_create(&tid, NULL, ThreadFunc, this);
_threads.push_back(tid);
}
}
// 线程入口函数:循环处理任务
static void* ThreadFunc(void* arg) {
ThreadPool* pool = (ThreadPool*)arg;
while (true) {
LockGuard lock(pool->_task_mutex);
// 任务队列为空且线程池运行中,等待任务
while (pool->_task_queue.empty() && pool->_is_running) {
pool->_task_cond.Wait(pool->_task_mutex);
}
// 线程池停止且任务队列为空,退出线程
if (!pool->_is_running && pool->_task_queue.empty()) {
break;
}
// 处理任务
Task task = pool->_task_queue.front();
pool->_task_queue.pop();
lock.~LockGuard(); // 提前解锁,避免任务执行时占用锁
task(); // 执行任务
}
return NULL;
}
private:
static ThreadPool* _instance; // 单例实例
static Mutex _singleton_mutex; // 保护单例创建的互斥量
int _thread_num; // 线程数量
std::vector<pthread_t> _threads; // 线程队列
std::queue<Task> _task_queue; // 任务队列
Mutex _task_mutex; // 保护任务队列的互斥量
Cond _task_cond; // 通知任务到达的条件变量
bool _is_running; // 线程池运行状态
};
// 初始化静态成员
ThreadPool* ThreadPool::_instance = nullptr;
Mutex ThreadPool::_singleton_mutex;
4.3 线程池的使用示例
#include <iostream>
#include <unistd.h>
#include "ThreadPool.hpp"
// 测试任务1:打印信息
void TestTask1() {
printf("线程[%lu]:执行测试任务1\n", pthread_self());
usleep(100000);
}
// 测试任务2:计算加法
void TestTask2(int a, int b) {
printf("线程[%lu]:%d + %d = %d\n", pthread_self(), a, b, a + b);
usleep(200000);
}
int main() {
// 获取线程池实例(10个线程)
ThreadPool* pool = ThreadPool::GetInstance(10);
// 提交任务(用std::bind绑定参数)
for (int i = 0; i < 20; ++i) {
if (i % 2 == 0) {
pool->SubmitTask(TestTask1);
} else {
pool->SubmitTask(std::bind(TestTask2, i, i + 1));
}
}
// 等待任务完成
sleep(5);
// 停止线程池并等待线程退出
pool->Stop();
pool->Wait();
return 0;
}
五、线程安全与死锁:避坑指南
5.1 线程安全与可重入
线程安全:多个线程并发访问函数 / 资源时,结果始终正确(如加锁保护共享资源)。
可重入函数:函数被多个执行流(线程 / 信号)重入时,结果正确(如不使用全局变量、不调用不可重入函数)。
关系:可重入函数一定是线程安全的,但线程安全函数不一定是可重入的(如加锁的函数若未释放锁则不可重入)。
常见线程不安全场景:
不保护共享变量的函数(如未加锁的全局变量操作)。
使用静态 / 全局变量的函数(如strtok)。
调用不可重入函数的函数(如调用malloc的函数)。
5.2 死锁:多线程的 “致命陷阱”
死锁是指多个线程互相持有对方需要的资源,导致永久阻塞的状态。
5.2.1 死锁的四个必要条件
- 互斥条件:资源只能被一个线程持有。
- 请求与保持条件:线程持有资源的同时,请求其他资源。
- 不剥夺条件:资源不能被强行剥夺。
- 循环等待条件:线程间形成资源请求的循环链。
5.2.2 避免死锁的方法
破坏循环等待条件:按固定顺序申请资源(如线程 1 先申请锁 A 再申请锁 B,线程 2 也按此顺序)。
一次性申请所有资源:用pthread_mutex_trylock或std::lock一次性获取所有锁。
超时释放:申请资源时设置超时,超时后释放已持有资源。
示例:按固定顺序申请锁避免死锁
// 线程1:先申请锁A,再申请锁B
void* thread1(void* arg) {
pthread_mutex_lock(&lockA);
pthread_mutex_lock(&lockB);
// 访问资源
pthread_mutex_unlock(&lockB);
pthread_mutex_unlock(&lockA);
return NULL;
}
// 线程2:同样先申请锁A,再申请锁B(避免循环等待)
void* thread2(void* arg) {
pthread_mutex_lock(&lockA);
pthread_mutex_lock(&lockB);
// 访问资源
pthread_mutex_unlock(&lockB);
pthread_mutex_unlock(&lockA);
return NULL;
}
六、总结与实践建议
线程同步与互斥是多线程编程的核心,掌握它们能有效解决资源竞争和执行顺序问题。以下是关键总结和实践建议:
6.1 核心总结
- 互斥量:用于保护临界资源,确保同一时间只有一个线程进入临界区(适合 “独占资源” 场景)。
- 条件变量:用于线程间的 “等待 - 通知”,实现同步(如生产者 - 消费者模型)。
- 信号量:通用同步工具,支持多资源并发访问(适合 “共享资源池” 场景)。
- 线程池:复用线程减少开销,适合大量短任务的场景(如 Web 服务器)。
- 死锁避免:按固定顺序申请资源、一次性申请所有资源,破坏死锁的必要条件。
6.2 实践建议
- 优先使用 RAII 封装:用
LockGuard自动管理锁的生命周期,避免忘记解锁。 - 避免全局变量:用线程局部存储(
__thread)或函数参数传递数据,减少共享资源。 - 选择合适的同步工具:
- 独占资源用互斥量。
- 线程间等待通知用条件变量。
- 多资源并发访问用信号量。
- 调试技巧:
- 用
pstack 进程ID查看线程调用栈,定位死锁。 - 用
valgrind --tool=helgrind检测线程安全问题。 - 日志中记录线程 ID 和操作,便于追踪并发问题。
- 用
1315

被折叠的 条评论
为什么被折叠?



