【Linux编程】线程同步与互斥
一、线程互斥:解决资源竞争的核心
1.1 核心概念铺垫
多线程并发访问共享资源时,若缺乏保护机制,会导致数据一致性问题。先明确几个关键概念:
- 共享资源:多个线程均可访问的数据或资源(如全局变量、硬件设备)。
- 临界资源:需要被保护的共享资源(如售票系统中的剩余票数)。
- 临界区:线程中访问临界资源的代码段。
- 互斥:保证同一时刻只有一个线程进入临界区,是线程安全的基础。
- 原子性:操作不可被中断,要么完整执行,要么不执行(如CPU的swap指令)。
1.2 为什么需要互斥?实战反例
先看一个无保护的售票系统代码,感受资源竞争带来的问题:
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100; // 共享资源:剩余票数
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
if (ticket > 0) {
usleep(1000); // 模拟业务处理延迟
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
}
int main(void) {
pthread_t t1, t2, t3, t4;
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
模拟运行结果(异常):
thread 4 sells ticket:100
thread 4 sells ticket:1
thread 2 sells ticket:0
thread 1 sells ticket:-1
thread 3 sells ticket:-2
问题分析:
if (ticket > 0)判断后,线程可能被切换,导致多个线程同时进入临界区。ticket--并非原子操作,实际对应三条汇编指令(load→update→store),过程中可能被中断。
1.3 互斥量(mutex):Linux的锁机制
Linux提供互斥量(mutex)实现线程互斥,核心是通过锁机制保证临界区原子执行。
1.3.1 互斥量核心接口
| 操作 | 函数声明 | 说明 |
|---|---|---|
| 静态初始化 | pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER |
简单场景使用,无需手动销毁 |
| 动态初始化 | int pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *attr) |
attr传NULL表示默认属性 |
| 销毁 | int pthread_mutex_destroy(pthread_mutex_t *mutex) |
仅销毁动态初始化的互斥量,已加锁的互斥量不可销毁 |
| 加锁 | int pthread_mutex_lock(pthread_mutex_t *mutex) |
阻塞式加锁,若锁被占用则等待 |
| 解锁 | int pthread_mutex_unlock(pthread_mutex_t *mutex) |
释放锁,唤醒等待的线程 |
1.3.2 改进版售票系统(互斥保护)
cpp
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
int ticket = 100;
pthread_mutex_t mutex; // 定义互斥量
void *route(void *arg) {
char *id = (char*)arg;
while (1) {
pthread_mutex_lock(&mutex); // 加锁
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
pthread_mutex_unlock(&mutex); // 解锁
} else {
pthread_mutex_unlock(&mutex); // 解锁(避免死锁)
break;
}
}
return nullptr;
}
int main(void) {
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL); // 动态初始化
pthread_create(&t1, NULL, route, (void*)"thread 1");
pthread_create(&t2, NULL, route, (void*)"thread 2");
pthread_create(&t3, NULL, route, (void*)"thread 3");
pthread_create(&t4, NULL, route, (void*)"thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
pthread_mutex_destroy(&mutex); // 销毁互斥量
return 0;
}
运行结果(正常):票数从100递减到1,无负数,无重复售票。
1.4 互斥量实现原理
互斥量的原子性依赖CPU的特殊指令(如swap或exchange),核心伪代码逻辑:
asm
lock:
movb $0, %al
xchgb %al, mutex # 原子交换:寄存器与内存数据互换
if (%al > 0) {
挂起等待;
goto lock;
}
return 0;
unlock:
movb $1, mutex # 释放锁
唤醒等待线程;
return 0;
1.5 C++封装:RAII风格锁
手动加锁解锁容易遗漏(如异常退出时),用RAII(资源获取即初始化)风格封装,自动管理锁的生命周期:
cpp
// Lock.hpp
#pragma once
#include <pthread.h>
namespace LockModule {
class Mutex {
public:
Mutex(const Mutex &) = delete; // 禁用拷贝
Mutex &operator=(const Mutex &) = delete;
Mutex() { pthread_mutex_init(&_mutex, nullptr); }
~Mutex() { pthread_mutex_destroy(&_mutex); }
void Lock() { pthread_mutex_lock(&_mutex); }
void Unlock() { pthread_mutex_unlock(&_mutex); }
pthread_mutex_t *GetMutexOriginal() { return &_mutex; }
private:
pthread_mutex_t _mutex;
};
// RAII锁管理
class LockGuard {
public:
LockGuard(Mutex &mutex) : _mutex(mutex) { _mutex.Lock(); }
~LockGuard() { _mutex.Unlock(); }
private:
Mutex &_mutex;
};
}
使用封装后的锁:
cpp
#include "Lock.hpp"
using namespace LockModule;
int ticket = 1000;
Mutex mutex;
void *route(void *arg) {
char *id = (char *)arg;
while (1) {
LockGuard lockguard(mutex); // 自动加锁
if (ticket > 0) {
usleep(1000);
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
} else {
break;
}
}
return nullptr;
}
二、线程同步:实现有序协作
2.1 同步的核心意义
互斥解决了"资源竞争"问题,但未解决"执行顺序"问题。同步是在保证线程安全的前提下,让线程按特定顺序执行,避免饥饿问题。
- 竞态条件:因线程执行时序不确定导致的程序异常。
- 条件变量:实现线程同步的核心机制,允许线程等待某个条件满足后再执行。
2.2 条件变量核心接口
| 操作 | 函数声明 | 说明 |
|---|---|---|
| 初始化 | int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *attr) |
attr传NULL表示默认属性 |
| 销毁 | int pthread_cond_destroy(pthread_cond_t *cond) |
销毁条件变量 |
| 等待 | int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) |
释放互斥量并等待,被唤醒后重新获取互斥量 |
| 唤醒单个线程 | int pthread_cond_signal(pthread_cond_t *cond) |
唤醒等待队列中的一个线程 |
| 唤醒所有线程 | int pthread_cond_broadcast(pthread_cond_t *cond) |
唤醒等待队列中的所有线程 |
2.3 条件变量使用场景:简单通知机制
cpp
#include <iostream>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
using namespace std;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void *active(void *arg) {
string name = static_cast<const char*>(arg);
while (true) {
pthread_mutex_lock(&mutex);
pthread_cond_wait(&cond, &mutex); // 等待通知
cout << name << " 活动..." << endl;
pthread_mutex_unlock(&mutex);
}
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, active, (void*)"thread-1");
pthread_create(&t2, NULL, active, (void*)"thread-2");
sleep(3); // 确保线程已启动
while (true) {
pthread_cond_broadcast(&cond); // 唤醒所有线程
sleep(1);
}
pthread_join(t1, NULL);
pthread_join(t2, NULL);
return 0;
}
运行结果:两个线程每隔1秒被唤醒并执行打印操作。
2.4 关键问题:为什么pthread_cond_wait需要互斥量?
- 条件依赖共享资源状态,互斥量保护共享资源的读写安全。
- 解锁和等待必须是原子操作,避免"信号丢失"(解锁后、等待前信号被发送)。
pthread_cond_wait内部逻辑:先释放互斥量→等待信号→被唤醒后重新获取互斥量。
2.5 条件变量使用规范
cpp
// 等待条件(消费者)
pthread_mutex_lock(&mutex);
while (条件为假) { // 用while避免伪唤醒
pthread_cond_wait(&cond, &mutex);
}
// 处理逻辑
pthread_mutex_unlock(&mutex);
// 发送信号(生产者)
pthread_mutex_lock(&mutex);
设置条件为真;
pthread_cond_signal(&cond); // 或broadcast
pthread_mutex_unlock(&mutex);
2.6 条件变量封装
cpp
// Cond.hpp
#pragma once
#include <pthread.h>
#include "Lock.hpp"
namespace CondModule {
using namespace LockModule;
class Cond {
public:
Cond() { pthread_cond_init(&_cond, nullptr); }
~Cond() { pthread_cond_destroy(&_cond); }
void Wait(Mutex &mutex) {
pthread_cond_wait(&_cond, mutex.GetMutexOriginal());
}
void Notify() { pthread_cond_signal(&_cond); }
void NotifyAll() { pthread_cond_broadcast(&_cond); }
private:
pthread_cond_t _cond;
};
}
三、生产者消费者模型:同步与互斥的综合应用
3.1 模型核心思想(321原则)
- 3种关系:生产者与生产者(互斥)、消费者与消费者(互斥)、生产者与消费者(同步+互斥)。
- 2个角色:生产者(生产数据)、消费者(处理数据)。
- 1个容器:阻塞队列(缓冲区),解耦生产者和消费者。
3.2 模型优势
- 解耦:生产者和消费者无需直接通信,通过队列间接交互。
- 支持并发:生产者和消费者可独立并发执行。
- 平衡负载:缓冲队列缓解生产者和消费者处理速度不匹配的问题。
3.3 基于阻塞队列的实现
3.3.1 阻塞队列封装(BlockQueue.hpp)
cpp
#ifndef __BLOCK_QUEUE_HPP__
#define __BLOCK_QUEUE_HPP__
#include <iostream>
#include <queue>
#include <pthread.h>
template <typename T>
class BlockQueue {
private:
bool IsFull() { return _block_queue.size() == _cap; }
bool IsEmpty() { return _block_queue.empty(); }
public:
BlockQueue(int cap) : _cap(cap), _productor_wait_num(0), _consumer_wait_num(0) {
pthread_mutex_init(&_mutex, nullptr);
pthread_cond_init(&_product_cond, nullptr);
pthread_cond_init(&_consum_cond, nullptr);
}
~BlockQueue() {
pthread_mutex_destroy(&_mutex);
pthread_cond_destroy(&_product_cond);
pthread_cond_destroy(&_consum_cond);
}
// 生产者入队
void Enqueue(T &in) {
pthread_mutex_lock(&_mutex);
while (IsFull()) { // 队列满则等待
_productor_wait_num++;
pthread_cond_wait(&_product_cond, &_mutex);
_productor_wait_num--;
}
_block_queue.push(in);
if (_consumer_wait_num > 0) {
pthread_cond_signal(&_consum_cond); // 通知消费者
}
pthread_mutex_unlock(&_mutex);
}
// 消费者出队
void Pop(T *out) {
pthread_mutex_lock(&_mutex);
while (IsEmpty()) { // 队列空则等待
_consumer_wait_num++;
pthread_cond_wait(&_consum_cond, &_mutex);
_consumer_wait_num--;
}
*out = _block_queue.front();
_block_queue.pop();
if (_productor_wait_num > 0) {
pthread_cond_signal(&_product_cond); // 通知生产者
}
pthread_mutex_unlock(&_mutex);
}
private:
std::queue<T> _block_queue; // 存储数据的队列
int _cap; // 队列容量上限
pthread_mutex_t _mutex; // 保护队列的互斥量
pthread_cond_t _product_cond;// 生产者条件变量
pthread_cond_t _consum_cond; // 消费者条件变量
int _productor_wait_num; // 等待的生产者数量
int _consumer_wait_num; // 等待的消费者数量
};
#endif
3.3.2 任务类型定义
cpp
// 任务类型:支持任意可调用对象
using Task = std::function<void()>;
3.4 基于POSIX信号量的环形队列实现
信号量可简化同步逻辑,环形队列适合固定大小的缓冲区场景:
3.4.1 信号量封装(Sem.hpp)
cpp
#pragma once
#include <semaphore.h>
class Sem {
public:
Sem(int n) { sem_init(&_sem, 0, n); }
~Sem() { sem_destroy(&_sem); }
void P() { sem_wait(&_sem); } // 等待(P操作:减1)
void V() { sem_post(&_sem); } // 发布(V操作:加1)
private:
sem_t _sem;
};
3.4.2 环形队列实现(RingQueue.hpp)
cpp
#pragma once
#include <iostream>
#include <vector>
#include <pthread.h>
#include "Sem.hpp"
template<typename T>
class RingQueue {
private:
void Lock(pthread_mutex_t &mutex) { pthread_mutex_lock(&mutex); }
void Unlock(pthread_mutex_t &mutex) { pthread_mutex_unlock(&mutex); }
public:
RingQueue(int cap) : _ring_queue(cap), _cap(cap), _room_sem(cap), _data_sem(0),
_productor_step(0), _consumer_step(0) {
pthread_mutex_init(&_productor_mutex, nullptr);
pthread_mutex_init(&_consumer_mutex, nullptr);
}
~RingQueue() {
pthread_mutex_destroy(&_productor_mutex);
pthread_mutex_destroy(&_consumer_mutex);
}
// 生产者入队
void Enqueue(const T &in) {
_room_sem.P(); // 等待空闲空间
Lock(_productor_mutex);
_ring_queue[_productor_step++] = in;
_productor_step %= _cap; // 环形逻辑
Unlock(_productor_mutex);
_data_sem.V(); // 通知有新数据
}
// 消费者出队
void Pop(T *out) {
_data_sem.P(); // 等待数据
Lock(_consumer_mutex);
*out = _ring_queue[_consumer_step++];
_consumer_step %= _cap; // 环形逻辑
Unlock(_consumer_mutex);
_room_sem.V(); // 通知有空闲空间
}
private:
std::vector<T> _ring_queue; // 环形队列容器
int _cap; // 容量上限
int _productor_step; // 生产者下标
int _consumer_step; // 消费者下标
Sem _room_sem; // 空闲空间信号量
Sem _data_sem; // 已用数据信号量
pthread_mutex_t _productor_mutex; // 生产者互斥锁
pthread_mutex_t _consumer_mutex; // 消费者互斥锁
};
四、线程池设计:高效管理线程资源
4.1 线程池核心概念
线程池是预先创建一定数量的线程,等待处理任务的设计模式,优势如下:
- 避免频繁创建销毁线程的开销。
- 控制线程数量,防止资源耗尽。
- 提高任务响应速度,线程可复用。
4.2 线程池核心组件
- 线程队列:存储预先创建的线程。
- 任务队列:存储待执行的任务。
- 互斥量:保护任务队列的线程安全。
- 条件变量:通知线程有新任务到达。
- 控制变量:标记线程池是否运行。
4.3 线程池实现(ThreadPool.hpp)
结合日志系统和单例模式,实现生产级线程池:
cpp
#pragma once
#include <iostream>
#include <vector>
#include <queue>
#include <memory>
#include <pthread.h>
#include <functional>
#include "Log.hpp"
#include "Lock.hpp"
#include "Cond.hpp"
using namespace ThreadModule;
using namespace CondModule;
using namespace LockModule;
using namespace LogModule;
const static int gdefaultthreadnum = 10;
template <typename T>
class ThreadPool {
private:
ThreadPool(int threadnum = gdefaultthreadnum) : _threadnum(threadnum),
_waitnum(0), _isrunning(false) {
LOG(LogLevel::INFO) << "ThreadPool Construct()";
}
// 禁用拷贝
ThreadPool<T> &operator=(const ThreadPool<T> &) = delete;
ThreadPool(const ThreadPool<T> &) = delete;
// 线程执行函数
void HandlerTask() {
std::string name = GetThreadNameFromNptl();
LOG(LogLevel::INFO) << name << " is running...";
while (true) {
_mutex.Lock();
// 等待任务或线程池停止
while (_task_queue.empty() && _isrunning) {
_waitnum++;
_cond.Wait(_mutex);
_waitnum--;
}
// 线程池停止且任务队列为空,退出线程
if (_task_queue.empty() && !_isrunning) {
_mutex.Unlock();
break;
}
// 取出任务并执行
T t = _task_queue.front();
_task_queue.pop();
_mutex.Unlock();
LOG(LogLevel::DEBUG) << name << " get a task";
t(); // 执行任务
}
}
public:
// 单例模式:双重检查锁定
static ThreadPool<T> *GetInstance() {
if (nullptr == _instance) {
LockGuard lockguard(_lock);
if (nullptr == _instance) {
_instance = new ThreadPool<T>();
_instance->InitThreadPool();
_instance->Start();
LOG(LogLevel::DEBUG) << "创建线程池单例";
}
}
LOG(LogLevel::DEBUG) << "获取线程池单例";
return _instance;
}
// 初始化线程池:创建线程(不启动)
void InitThreadPool() {
for (int num = 0; num < _threadnum; num++) {
_threads.emplace_back(std::bind(&ThreadPool::HandlerTask, this));
LOG(LogLevel::INFO) << "init thread " << _threads.back().Name() << " done";
}
}
// 启动所有线程
void Start() {
_isrunning = true;
for (auto &thread : _threads) {
thread.Start();
LOG(LogLevel::INFO) << "start thread " << thread.Name() << " done";
}
}
// 停止线程池
void Stop() {
_mutex.Lock();
_isrunning = false;
_cond.NotifyAll(); // 唤醒所有等待线程
_mutex.Unlock();
LOG(LogLevel::DEBUG) << "线程池退出中...";
}
// 等待所有线程退出
void Wait() {
for (auto &thread : _threads) {
thread.Join();
LOG(LogLevel::INFO) << thread.Name() << " 退出...";
}
}
// 任务入队
bool Enqueue(const T &t) {
bool ret = false;
_mutex.Lock();
if (_isrunning) {
_task_queue.push(t);
if (_waitnum > 0) {
_cond.Notify(); // 唤醒等待线程
}
LOG(LogLevel::DEBUG) << "任务入队列成功";
ret = true;
}
_mutex.Unlock();
return ret;
}
private:
int _threadnum; // 线程数量
std::vector<Thread> _threads; // 线程队列
std::queue<T> _task_queue; // 任务队列
Mutex _mutex; // 保护队列的互斥量
Cond _cond; // 通知条件变量
int _waitnum; // 等待任务的线程数
bool _isrunning; // 线程池运行状态
static ThreadPool<T> *_instance; // 单例实例
static Mutex _lock; // 单例锁
};
// 静态成员初始化
template <typename T>
ThreadPool<T> *ThreadPool<T>::_instance = nullptr;
template <typename T>
Mutex ThreadPool<T>::_lock;
4.4 线程池测试代码
cpp
#include <iostream>
#include <functional>
#include <unistd.h>
#include "ThreadPool.hpp"
using task_t = std::function<void()>;
void DownLoad() {
std::cout << "this is a task" << std::endl;
}
int main() {
ENABLE_CONSOLE_LOG_STRATEGY(); // 启用控制台日志
int cnt = 10;
while (cnt--) {
ThreadPool<task_t>::GetInstance()->Enqueue(DownLoad);
sleep(1);
}
ThreadPool<task_t>::GetInstance()->Stop();
sleep(5);
ThreadPool<task_t>::GetInstance()->Wait();
return 0;
}
编译命令 :g++ main.cc -std=c++17 -lpthread
运行结果:线程池创建10个线程,每隔1秒接收一个任务并执行,最后正常退出。
五、线程安全与可重入
5.1 核心定义
- 线程安全:多个线程并发访问时,程序执行结果一致,无数据竞争。
- 可重入:同一函数被多个执行流(线程或信号)重复调用时,结果正确。
5.2 常见场景对比
| 特性 | 线程安全 | 可重入 |
|---|---|---|
| 核心关注 | 多线程访问共享资源的安全性 | 函数本身的设计健壮性 |
| 关系 | 可重入函数一定是线程安全的 | 线程安全函数不一定是可重入的 |
| 常见不安全情况 | 未保护的共享变量、静态变量读写 | 调用malloc/free、静态变量操作 |
| 常见安全情况 | 仅访问局部变量、原子操作 | 无全局/静态变量、不调用不可重入函数 |
5.3 实战建议
- 避免在函数中使用全局变量和静态变量。
- 若必须使用共享资源,需通过互斥量保护。
- 可重入函数优先(如仅使用栈空间、参数传递数据)。
六、死锁与避免:多锁场景的坑
6.1 死锁的四个必要条件
- 互斥条件:资源只能被一个线程占用。
- 请求与保持条件:线程持有资源时,请求其他资源并阻塞。
- 不剥夺条件:资源不能被强行剥夺。
- 循环等待条件:线程间形成资源等待循环。
6.2 避免死锁的关键方法
- 破坏循环等待:统一加锁顺序(如按资源地址排序)。
- 资源一次性分配:申请所有需要的资源后再执行。
- 超时机制:加锁时设置超时,超时后释放已持有资源。
- 避免锁未释放:使用RAII风格锁,确保异常时也能解锁。
示例:统一加锁顺序避免死锁
cpp
// 错误:加锁顺序不一致
// 线程A:lock(mtx1) → lock(mtx2)
// 线程B:lock(mtx2) → lock(mtx1)
// 正确:按固定顺序加锁
void func() {
pthread_mutex_lock(&mtx1); // 先加锁1
pthread_mutex_lock(&mtx2); // 再加锁2
// 业务逻辑
pthread_mutex_unlock(&mtx2);
pthread_mutex_unlock(&mtx1);
}
七、总结与进阶方向
本文从线程互斥、同步的基础概念出发,逐步深入到生产者消费者模型、线程池设计等实战场景,覆盖了Linux线程编程的核心知识点。关键总结:
- 互斥用mutex,解决资源竞争;同步用条件变量/信号量,解决执行顺序。
- 生产者消费者模型是同步与互斥的经典应用,解耦线程间交互。
- 线程池通过复用线程提高效率,适合高并发短任务场景。
- 线程安全和可重入是编写可靠多线程程序的基础。
- 死锁避免的核心是破坏四个必要条件中的任意一个。
进阶学习方向
- 深入学习红黑树、哈希表等数据结构,理解STL容器的线程安全性。
- 研究高级同步机制:读写锁、自旋锁、屏障(barrier)。
- 探索分布式场景下的同步方案:分布式锁(如Redis实现)。
- 性能优化:锁粒度控制、无锁编程(CAS操作)。