多线程
线程概念
进程概念
把一个可执行程序运行起来就是进程
代码和数据+内核数据结构(PCB)
Linux下线程实现
进程 :资源分配的基本单位
线程 :CPU 调度的基本单位
Linux 没有真正意义上的线程结构体,而是用 轻量级进程(LWP) 实现,复用进程的 PCB 结构。
线程间的独有与共享
共享(同一进程内线程共享)
代码和数据
自定义函数和全局变量
文件描述符表
信号处理方式
工作目录
独有(每个线程私有)
一组寄存器上下文(核心,调度切换用)
独立的栈空间(核心,保证调用不混乱)
线程 ID(TID)
信号屏蔽字(block 位图)
调度优先级
私有 errno
多线程/多进程任务处理的优缺点
优点
创建线程代价远小于创建进程
线程切换开销更低、更快
线程占用资源远少于进程
能更好利用多核 CPU 实现并行
计算密集型任务可拆分到多线程
可同时等待多个不同 IO 操作
缺点
会有额外调度切换开销,轻微性能损失
健壮性低,全局变量容易被意外修改
一个线程异常崩溃,整个进程全部崩溃
编程难度更高,需要处理同步互斥问题
线程控制
线程创建
线程在进程的虚拟地址空间内,拥有独立的栈结构
线程的局部数据、栈空间,存储在进程的 mmap 区域
同一进程内所有线程共享大部分资源,只独有栈、寄存器等少量数据
线程创建函数
c
int pthread_create(
pthread_t *thread, // 输出型参数:返回创建好的线程ID
const pthread_attr_t *attr, // 线程属性,默认填 NULL
void *(*start_routine)(void *), // 线程入口函数(函数指针)
void *arg // 传给线程函数的参数
);
线程终止
线程退出专用函数:
c
void pthread_exit(void *retval);
作用:退出当前线程,不影响进程内其他线程
退出数据可通过返回值传递,供pthread_join接收
线程等待
线程退出后,如果不被等待回收,资源会残留在进程地址空间中(类似僵尸进程)
已退出线程的资源不会被新线程自动复用,必须手动等待
等待接口:
c
int pthread_join(pthread_t thread, void **retval);
作用:阻塞等待指定线程退出,回收其资源、获取退出信息
线程分离
功能:将线程设置为分离状态,无需主线程pthread_join
线程退出后,操作系统自动回收资源
接口:
c
int pthread_detach(pthread_t thread);
适用场景:不关心线程返回值、不需要等待线程退出
注意:分离后的线程不能再被 join,否则报错
线程安全
概念
多个线程对于临界资源的争抢访问操作,但不会造成数据的二义性
如何实现
互斥
保证安全性(同一时间只有一个线程访问)
同步
保证合理性(按顺序执行,避免条件不满足)
互斥实现
互斥锁
信号量
信号量初始化为 1 时就是互斥锁
申请资源时信号量 -1,不足则阻塞
释放资源时信号量 +1,唤醒等待线程
同步实现
条件变量
判断流程和循环判断是否满足条件
信号量
生产消费者模型
full_count:记录缓冲区中已有产品数量
empty_slot:记录缓冲区可用空位数量
生产消费者模型
场景
并发编程的模型,用来解决多线程共享数据的问题
作用
- 解耦合
分离生产者和消费者的任务,使得它们可以独立工作,互不影响
2. 支持忙闲不均
有缓冲区,有同步的概念
3. 支持并发
生产者和消费者可以同时执行,提高系统的效率,允许存在处理速度的不同
实现
线程安全队列
生产者消费者线程创建
总结:三二一原则
三个关系
生产者与生产者:互斥
消费者与消费者:互斥
生产者与消费者:互斥 + 同步
互斥:不能同时生产和消费
同步:必须先生产,后消费
两种角色
生产者
消费者
一个交易场所
共享内存、队列、缓冲区
读者写者问题模型
读写锁
概念
支持多个读者同时读
只允许一个写者独占写
读共享、写独占
实现原理
读锁获取
-
检查写锁是否持有,如果没有持有就允许读锁
-
如果写锁持有,或者读锁满了,就阻塞等待
-
当满足条件后会唤醒阻塞的线程
写锁获取 -
先检查是否有读锁,再检查是否有写锁,必须独占
-
如果不满足,就阻塞等待
-
如果满足了,就唤醒阻塞的线程
读锁释放
如果读完了,就释放读锁,如果没人读了,就唤醒阻塞的写进程
写锁释放
如果写完了,就释放写锁,此时写进程和读进程都可以申请,根据特定策略进行发放
优先级
通常按请求先后顺序分配锁。
自旋锁
概念
线程申请锁失败时,不挂起、不切换上下文,而是原地循环自旋等待,直到锁可用。
实现原理
循环申请某个锁,直到申请成功
使用场景
临界区极短,锁等待时间很短的场景,避免切换开销。
悲观乐观锁
悲观锁
默认认为一定会冲突,每次都先加锁再访问。
互斥锁、读写锁、信号量基本都是悲观锁。
乐观锁
默认冲突概率很低,先修改数据,最后提交时再检测是否冲突。
冲突则回滚重试,适合读多写少、冲突极少的场景。
线程池
概念
预先创建一组可复用的线程 ,当有新任务来就选择一个空闲线程执行,任务结束回到线程池等待下一次分配
解决的问题
解决了线程创建和销毁的开销
方便于对于线程进行管理和控制
实现
管理模块:线程池控制结构(线程数组、任务队列、锁、条件变量等)
工作线程:预先创建好的一批线程,不断从任务队列取任务执行
任务队列:用链表 / 队列保存待执行任务
任务接口:统一的任务函数指针
流程:创建线程池 → 提交任务入队 → 线程竞争取任务 → 执行任务 → 执行完毕回到空闲
线程安全的单例模式
设计模式
单例是全局只存在一个实例。
单例模式概念
一个类只能创建一个对象,资源只加载一次,全局共用。
单例模式实现
饿汉模式
特点:main 函数开始前就已经创建好实例
优点:天然线程安全,无需加锁
缺点:程序启动就初始化,可能浪费资源
c
// 饿汉单例
class Singleton {
private:
static Singleton inst;
Singleton() {} // 构造私有
public:
static Singleton* getInstance() {
return &inst;
}
};
Singleton Singleton::inst;
懒汉模式
特点:第一次使用时才创建实例
优点:延时加载,节约启动资源
缺点:线程不安全,必须加锁保证安全
c
// 线程安全懒汉单例
class Singleton {
private:
static Singleton* inst;
static pthread_mutex_t mtx;
Singleton() {}
public:
static Singleton* getInstance() {
if (inst ==nullptr) { // 双检查锁
pthread_mutex_lock(&mtx);
if (inst== nullptr) {
inst = new Singleton;
}
pthread_mutex_unlock(&mtx);
}
return inst;
}
};
Singleton* Singleton::inst = nullptr;
pthread_mutex_t Singleton::mtx = PTHREAD_MUTEX_INITIALIZER;
STL容器安全
默认线程不安全
多线程同时读写必须自己加锁
智能指针
shared_ptr:引用计数本身是原子的,线程安全
但指向的对象不安全,需要自己保证