线程的互斥和同步 - 教程
1.线程的互斥
线程互斥的一些概念:
1.临界资源:多线程执行流共享的资源就叫做临界资源
2.临界区:每个线程内部,访问临界资源的代码,就叫做临界区
3.互斥:任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用
3.原子性(后面讨论如何实现):不会被任何调度机制打断的操作,该操作只有两态,要么完成要么未完成
1.1.现象and锁的引入

现象:最后的结果并不符合我们预期,最后的结果变成了负数
// 操作共享变量有问题的售票系统代码
#include
#include
#include
#include
#include
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, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
}
/* 一次典型执行结果(竞争问题导致异常):
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
*/
现在对上面代码的分析:
首先造成这个结果的主要原因:ticket--;操作并不是原子的
1.其实cpu来执行这段代码分成三步:
A.将内存中的值拿到寄存器中
B.在cpu中的寄存器堆ticket值进行计算
C.将计算的值返回给内存

2.假设此时有两个,N和M,这两个线程如果想进行ticket进行计算就必须经历ABC三个步骤,
因为此时有进程的调度,所以可能在执行N线程的时候,M线程被包含在N线程三个步骤之内,
所以当N线程结束的时候,造成只对ticket进行一次的--;

解决方式:加入锁
下面以加入全局锁为示例:
1.临界资源:被保护起来的资源

2.临界区:加锁和解锁的中间部分,其他部分就是非临界部分
对临界资源进行保护:本质起始就是用锁,来对临界区代码进行保护

代码示例:
#include
#include
#include // 包含 usleep 函数
// 全局共享资源:车票数量
int ticket = 10000;
// 全局互斥锁:保护 ticket 的访问
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void* route(void* arg) {
char* id = (char*)arg; // 线程标识(如 "thread 1")
while (1) {
// 1. 加锁:进入临界区,确保同一时间只有一个线程操作 ticket
pthread_mutex_lock(&lock);
if (ticket > 0) {
// 模拟耗时操作(放大并发问题,若无锁会导致数据混乱)
usleep(1000);
// 2. 业务逻辑:打印并修改票号
printf("%s sells ticket:%d\n", id, ticket);
ticket--;
// 3. 解锁:退出临界区,允许其他线程访问
pthread_mutex_unlock(&lock);
} else {
// 分支里也要解锁!否则线程退出时未解锁,导致死锁
pthread_mutex_unlock(&lock);
break; // 票卖完,退出循环
}
}
return nullptr;
}
int main() {
pthread_t t1, t2, t3, t4; // 示例:创建 4 个线程
// 创建线程,传递不同标识
pthread_create(&t1, NULL, route, "thread 1");
pthread_create(&t2, NULL, route, "thread 2");
pthread_create(&t3, NULL, route, "thread 3");
pthread_create(&t4, NULL, route, "thread 4");
// 等待所有线程结束
pthread_join(t1, NULL);
pthread_join(t2, NULL);
pthread_join(t3, NULL);
pthread_join(t4, NULL);
return 0;
}
问题:
问题1:加锁之后,在临界区内部,允许线程切换吗?切换了会怎么样?
允许切换的,但是不怎么样,其他所有线程必须等我跑完。
因为我当前线程,并没有释放锁我是持有锁被切换的,即便我不在!其他线程也得等我回来执行完代码释放锁,其他线程才能展开锁的竞争进入临界区
1.2.全局锁和局部锁
前提:竞争申请锁,多线程都得先看到锁,锁本身就是临界资源!申请锁的过程,必须是原子的
全局锁的使用:
全局锁API:
#include
// 1. 创建全局锁(静态初始化)
pthread_mutex_t global_mutex = PTHREAD_MUTEX_INITIALIZER;
int main() {
// 2. 使用锁
pthread_mutex_lock(&global_mutex);
// 临界区...
pthread_mutex_unlock(&global_mutex);
// 3. 可省略销毁(系统会回收),但建议加上
pthread_mutex_destroy(&global_mutex);
return 0;
}
代码示例:
#include
#include
// 全局锁(静态初始化)
pthread_mutex_t global_lock = PTHREAD_MUTEX_INITIALIZER;
int main() {
// 1. 使用全局锁
pthread_mutex_lock(&global_lock);
printf("全局锁被使用...\n");
pthread_mutex_unlock(&global_lock);
// 2. 不调用 pthread_mutex_destroy(&global_lock)
printf("程序退出,全局锁依赖系统回收资源\n");
return 0;
}
局部锁的使用:
局部锁API:
#include
void thread_func(void* arg) {
// 1. 创建局部锁(栈上)
pthread_mutex_t local_mutex;
pthread_mutex_init(&local_mutex, NULL); // 动态初始化
// 2. 使用锁
pthread_mutex_lock(&local_mutex);
// 临界区...
pthread_mutex_unlock(&local_mutex);
// 3. 必须手动销毁!否则内核资源泄漏
pthread_mutex_destroy(&local_mutex);
}
代码示例:
#include
#include
void func() {
pthread_mutex_t local_lock = PTHREAD_MUTEX_INITIALIZER;
pthread_mutex_lock(&local_lock);
printf("局部锁被使用...\n");
pthread_mutex_unlock(&local_lock);
// 关键:手动销毁局部锁,释放内核资源
pthread_mutex_destroy(&local_lock);
}
int main() {
for (int i = 0; i < 10000; i++) {
func(); // 多次调用,每次销毁,无资源泄漏
}
printf("程序退出,局部锁资源已正确释放\n");
return 0;
}
两者的区别:

1.3.锁的底层机制
有两种实现机制:
1.硬件级实现:关闭时钟中断,这样在临界区的时候,就不会发生进程的调度,但是可能会导致完成操作后时间中断无法唤醒而导致死机
2.软件级实现:

前提:只有一条汇编语句的操作那么它就是原子的
为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换

要了解软件级的锁的机制搞懂上面伪代码就行
1.在内存中存在一个mutex标志位,假设它没有被上锁的时候,里面存的是1
1:表示锁还在
0:反之
2.假设此时有两个线程A和B,A先执行 上锁代码的第一条语句,将cpu寄存器中的%al中的0和内存中的mutex中的1进行交换。
3.如果此时发生了进程的调度,B也拿着cpu寄存器中的%al中的0和内存中的mutex中的值进行交换,但是发现自己交换完后,自己拿到的值还是0,因为此时的1已经被线程A拿着呢。
4.所以此时的B只能等待挂起,在再发生发生进程调度调度到A进程的时候,A完成了A的代码,A再调用解锁,将1还给mutex,此时其他线程才能执行自己的代码。

2.线程同步
2.1.概念引入
饥饿问题:
其他线程拿不到锁,不高效
假设此时有A, B, C... 很多个线程,都想对count进行修改(此时count修改的代码是被上锁的,临界区),此时就会发生程序运行结束,发现修改count值的线程大部分都是A,其他线程只能干瞪眼的这样的一个现象,因为此时并没有排队的这一个说法
线程同步的概念:
线程同步的目标:
是 在多线程环境下保证数据一致性和执行顺序。通过合理使用互斥锁、条件变量等机制,可以避免数据竞争和死锁,构建高效且健壮的多线程程序。
条件变量的概念:
1. 核心概念:等待与通知
它解决了一个经典问题:线程需要等待某个条件满足才能继续执行
条件变量本质上是一个 线程阻塞队列,配合互斥锁使用,实现:
等待(Wait):线程阻塞并释放锁,直到被其他线程唤醒。
通知(Signal/Broadcast):唤醒一个或所有等待的线程。
2.2.条件变量的等待和唤醒
创建API:
#include
// 静态初始化(全局或静态变量)
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
// 动态初始化(堆上或栈上变量)
pthread_cond_t cond; // [out] 条件变量指针
pthread_cond_init(&cond, NULL); // NULL表示默认属性
// 销毁条件变量
pthread_cond_destroy(&cond);
cond: [in/out] 要销毁的条件变量指针
注意事项:
动态初始化的条件变量必须调用 destroy,否则资源泄漏。
销毁前需确保没有线程在该条件变量上等待,否则返回 EBUSY。
等待API:
// 无条件等待
int pthread_cond_wait(
pthread_cond_t *restrict cond, // [in/out] 条件变量指针
pthread_mutex_t *restrict mutex // [in/out] 关联的互斥锁(必须已加锁)
);
// 带超时的等待(绝对时间)
int pthread_cond_timedwait(
pthread_cond_t *restrict cond, // [in/out] 条件变量指针
pthread_mutex_t *restrict mutex, // [in/out] 关联的互斥锁(必须已加锁)
const struct timespec *restrict abstime // [in] 超时时间点(绝对时间)
);
唤醒API:
// 唤醒一个等待的线程
int pthread_cond_signal(
pthread_cond_t *cond // [in/out] 条件变量指针
);
// 唤醒所有等待的线程
int pthread_cond_broadcast(
pthread_cond_t *cond // [in/out] 条件变量指针
);
示例代码实现:
1.要使用条件变量,就一定加上互斥锁
2. 等待是需要等,什么条件才会等呢?就要对资源的数量进行判定。
3. 判定本身就是访问临界资源,判断一定是在临界区内部的.
4.判定结果,也一定在临界资源内部。所以,条件不满足要休眠,一定是在临界区内休眠的!
#include
#include
#include
#include
#include
#define NUM 5
int cnt = 1000;
pthread_mutex_t glock = PTHREAD_MUTEX_INITIALIZER; // 定义锁, 为什么一定要有锁??
pthread_cond_t gcond = PTHREAD_COND_INITIALIZER; // 定义条件变量
// 等待是需要等,什么条件才会等呢?票数为0,等待之前,就要对资源的数量进行判定。
// 判定本身就是访问临界资源!,判断一定是在临界区内部的.
// 判定结果,也一定在临界资源内部。所以,条件不满足要休眠,一定是在临界区内休眠的!
// 证明一件事情:条件变量,可以允许线程等待
// 可以允许一个线程唤醒在cond等待的其他线程, 实现同步过程
void *threadrun(void *args)
{
std::string name = static_cast(args);
while (true)
{
pthread_mutex_lock(&glock);
// 直接让对用的线程进行等待?? 临界资源不满足导致我们等待的!
pthread_cond_wait(&gcond, &glock); // glock在pthread_cond_wait之前,会被自动释放掉
std::cout threads;
for (int i = 0; i < NUM; i++)
{
pthread_t tid;
char *name = new char[64];
snprintf(name, 64, "thread-%d", i);
int n = pthread_create(&tid, nullptr, threadrun, name);
if (n != 0)
continue;
threads.push_back(tid);
sleep(1);
}
sleep(3);
// 每隔1s唤醒一个线程
while(true)
{
std::cout << "唤醒所有线程... " << std::endl;
pthread_cond_broadcast(&gcond);
// std::cout << "唤醒一个线程... " << std::endl;
// pthread_cond_signal(&gcond);
sleep(1);
}
for (auto &id : threads)
{
int m = pthread_join(id, nullptr);
(void)m;
}
return 0;
}
结果:就是入队列的顺序
thread-0 计算: 1000
thread-1 计算: 1001
thread-2 计算: 1002
thread-3 计算: 1003
thread-4 计算: 1004
唤醒所有线程...
thread-0 计算: 1005
thread-1 计算: 1006
thread-2 计算: 1007
thread-3 计算: 1008
thread-4 计算: 1009
唤醒所有线程...
thread-0 计算: 1010
thread-1 计算: 1011
thread-2 计算: 1012
thread-3 计算: 1013
thread-4 计算: 1014
唤醒所有线程...
...
2.3.生产者消费者模型
生产者消费者模型:
3种要素。 生产者s,消费者s,一个交易场所
2中角色:生产者角色和消费者角色(线程承担)
1个交易场所:以特定结构构成的一直"内存"空间
3种关系:
生产者之间:竞争关系,互斥关系
消费者和消费者之间:互斥关系
生产者和消费者之间: 互斥 和 同步

为什么要有生产者消费者模型?
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的
生产者消费者模型的两个优点:
1.解耦合

通常情况下的两个服务器之间:
编写A代码时候,肯定会涉及到B服务器的代码逻辑
编写B代码时候,肯定会涉及到A服务器的代码逻辑
但是服务器的代码是时常修改的,这就导致修改代码的时候,牵扯的关系太大了
所以就可以引进 --> 阻塞队列

A 和 队列交互
B 和 队列交互
A 和 B 不再直接交互了
A 的代码中就看不见 B了B 的代码中也看不见 A 了
A 的代码中和 B 的代码中只能看到队列
本来是 A 和 B 耦合现在成了 A 和 队列耦合,B 和队列耦合降低耦合,是为了让后续修改的时候,成本低,但是队列一般不会进行修改
2 . 削峰填谷

一般来说:
A 这边遇到一波流量激增此时每个请求都会转发给 B,
B 也会承担一样的压力很容易就把 B 给搞挂了
一般来说 A 这种上游的服务器,尤其是 入口的服务器,干的活更简单,
单个请求消耗的资源数少像 B 这种下游的服务器,通常承担更重的任务量,
复杂的计算/存储 工作,单个请求消耗的资源数更多
引入阻塞队列以后:

1.队列服务器, 针对单个请求,做的事情也少(存储,转发),所以队列服务器往往是可以抗很高的请求量
2.突发的时间也会短趁着峰值过去了,B 仍然继续消费数据利用波谷的时间,来赶紧消费之前积压的数据
之后B服务器就可以慢慢的处理队列服务器发来的请求指令
2.4.编写基于blockqueue的生产者消费者模型
阻塞队列(Blocking Queue)的介绍:
在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
1:1的生产者消费者模型:
BlockQueue头文件的实现:
#include
#include
#include
using namespace std;
#define defalutcap 10;
template
class BlockQueue
{
public:
BlockQueue(int cap = defalutcap)
: _cap(cap)
{
pthread_mutex_init(&_lock, nullptr);
pthread_cond_init(&_full_cond, nullptr);
pthread_cond_init(&_empty_cond, nullptr);
}
// 入队
void Equeue(cont T &in)
{
pthread_mutex_lock(&_lock);
// 队列满时,等待条件变量
// ·问题1:pthread cond wait是函数吗?有没有可能失败?pthread cond wait立即返回了
// ·问题2:pthread cond wait可能会因为,条件其实不满足,pthread cond wait·伪唤醒,所以此时要循环进行判断增强代码的健壮性
while (IsFull())
{
_csleep_num++; // 记录等待条件变量的线程数
// 重点1:pthread cond wait调用成功,挂起当前线程之前,要先自动释放锁!!
// 重点2:当线程被唤醒的时候,默认就在临界区内唤醒!要从pthread cond waitT成功返回,需要当前线程,重新申请 mutex锁!!!
// 重点3:如果我被唤醒,但是申请锁失败了??我就会在锁上阻塞等待!!!
pthread_cond_wait(&_full_cond, &_lock); // 等待条件变量
_csleep_num--; // 释放条件变量
}
// 此时暂时是空队列,可以入队
_q.push(in);
// 如果此时有生产者等待,进行通知
if (_csleep_num > 0)
{
pthread_cond_signal(&_empty_cond);
}
pthread_mutex_unlock(&_lock);
}
// 出队
T pop()
{
pthread_mutex_lock(&_lock);
while (IsEmpty())
{
_psleep_num++; // 记录等待条件变量的线程数
pthread_cond_wait(&_empty_num, &_lock);
_psleep_num--; // 释放条件变量
}
// 此时队列不空,可以出队
auto out = _q.front();
_q.pop();
if (_psleep_num > 0)
{
pthread_cond_signal(&_full_cond);
}
pthread_mutex_unlock(&_lock);
return out;
}
bool IsFull()
{
return _q.size() == _cap;
}
bool IsEmpty()
{
return _q.size() == 0;
}
~BlockQueu()
{
}
private:
queue _q; // 队列
int _cap; // 容量
pthread_mutex_t _lock; // 互斥锁
pthread_cond_t _full_cond; // 满条件变量
pthread_cond_t _empty_cond; // 空条件变量
int _csleep_num; // 消费者等待条件变量的线程
int _psleep_num; // 生产者等待条件变量的线程
}
BlockQueue头文件代码实现的注意点:
1.判断语句并不是简单的if判断语句,要换成while()循环进行判断,防止线程被伪唤醒,而导致其实队列的个数并不符合条件,而导致越界行为

2.将线程进行等待的时机
重点1:pthread cond wait调用成功,挂起当前线程之前,要先自动释放锁!
重点2:当线程被唤醒的时候,默认就在临界区内唤醒!要从pthread cond waitT成功返回,需要当前线程,重新申请 mutex锁!
重点3:如果我被唤醒,但是申请锁失败了??我就会在锁上阻塞等待(和其他线程争抢锁失败)!

Main.c的实现:
#include
#include "BlockQueue.hpp"
#include
using namespace std;
void *productor(void *arg)
{
// 先取出阻塞队列
BlockQueue *bq = (BlockQueue *)arg;
while (true)
{
int data = bq->Pop();
cout *bq = (BlockQueue *)arg;
while (true)
{
int data = 10;
bq->Equeue(data);
cout *bq = new BlockQueue();
// 创建生产者和消费者
pthread_t c, p;
pthread_create(&c, nullptr, consumer, bq);
pthread_create(&p, nullptr, productor, bq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
生产者消费者模型衍生问题:
1.如果我此时的生产者消费者模型 1:n or n:n 该如何?
很简单只需要改变main中的代码即可,其他的代码都不用改变。

2.在阻塞队列当中,存入的不仅仅可以是内置类型也可以是一个一个任务。

2.生产和消费的过程,本来就是互斥加锁串行 的啊.... 怎么能说它高效呢
其实我们说互斥加锁串行针对的动作只是生产者的入队列和消费者的出队列,这一部分占有的时间比例是很小的,但是当我们有10个任务,分别被10个消费者拿到,此时10个消费者是并行的处理这10个任务,此时的时间就会被压缩,任务的处理阶段才是占时占整个进程运行的大头
2.5.POSIX信号量的概念回顾
本质是一个计数器,是对特定资源的预订机制!
所有的线程,都得先看到sem,计数器,申请信号量:sem--,释放信号量 :sem++
信号量本质也是临界资源,
所以
P:-- :原子的
N: ++:原子的
当信号量为0的时候,代码就会进行阻塞。所以设计一个二元的信号量就可以模拟锁的特性
多线程使用资源,有两种场景:
1.将目标资源整体使用【mutex+2元信号量】--代表示例就是上面的blockqueue生产者消费模型
2.将目标资源按照不同的"块”,分批使用 【信号量】--就是我们等下要将讲的环形队列的生产者消费者模型
2.6.基于环形队列的生产者消费者模型概念
首先先回顾环形队列的概念:

有了上面环形队列的概念,假设此时我们的head指针是生产者,tail指针是消费者,我们定义如下的规则:
约定1:空,生产者先运行
约定2:满,消费者先运行
约定3:生产者不能把消费者套一个圈以上
约定4:消费者,不能超过生产者
根据上面的规则我们可以得出一下的结论:
结论1:环形队列,不为空 && 不为满,生产消费可以同时进行!
结论2:环形队列,为空|为满,生产和消费,需要同步互斥!
具有以上特性的生产者消费者模型,将使用信号量来实现,所以我们来介绍介绍关于信号量的接口
2.7.信号量的API
#include
int sem_init(sem_t *sem, int pshared, unsigned int value);
参数:
pshared:0表⽰线程间共享,⾮零表⽰进程间共享
value:信号量初始值
#include
int sem_destroy(sem_t *sem);
功能:等待信号量,会将信号量的值减1
int sem_wait(sem_t *sem); //P()
功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。
int sem_post(sem_t *sem);//V()
2.8.基于环形队列的生产者消费者模型代码实现
RingQueue类的实现
#include
#include
#include
using namespace std;
const int default_cap=10;
template
class RingQueue{
public:
RingQueue(int cap=default_cap)
:_rq(cap),_cap(cap),_p_step(0),_c_step(0)
{
// 初始化信号量,第一个参数是要初始化的信号量,第二个参数 0 表示线程间共享,第三个参数是初始值
sem_init(&_blank_sem, 0, cap);
sem_init(&_data_sem, 0, 0);
}
void Equeue(T &in){
//生产者:
//1.申请信号量,空位置
sem_wait(&_blank_sem);
//2.生产数据
_rq[_p_step]=in;
//3.更新下标,并维护环形特性
_p_step=(++_p_step)%_cap;
//4.释放信号量,有数据
sem_post(&_data_sem);
}
T Pop(){
//消费者:
//1.申请信号量,有数据
sem_wait(&_data_sem);
//2.消费数据
T out=_rq[_c_step];
//3.更新下标,并维护环形特性
_c_step=(++_c_step)%_cap;
//4.释放信号量,空位置
sem_post(&_blank_sem);
return out;
}
~RingQueue(){
sem_destroy(&_blank_sem);
sem_destroy(&_data_sem);
}
private:
vector _rq;
int _cap;
//生产者
sem_t _blank_sem;//空闲
int _p_step;
//消费者
sem_t _data_sem;//有数据
int _c_step;
}
核心代码部分:
此时的代码就满足
1:环形队列,不为空 && 不为满,生产消费可以同时进行!
2:环形队列,为空|为满,生产和消费,需要同步互斥!,会在申请信号量的时候等待

上面的针对的只是1:1的生产者消费者模型,那么此时我想要 n:n?
此时我们根据需求给出条件的限制,此时是N:N的模型,那么就会发生生产者和生产者,消费者和消费者之间会产生竞争的关系。
所以只要根据它们之间的竞争加上锁即可。
定义两把锁,一把锁用于生产者和生产者之间的竞争,另一把锁用于消费者和消费者之间的竞争

代码核心改动:

问题:
先进行加锁再进行位置的预定操作肯定是正确的,如果反过来呢?
当然是可以的,这就相当于买票和和排队的例子
1.先加锁:我不管我买没买到我都进行排队
2.后加锁:我先进行买票,没买到票我就不排队,买到票再排队,这样我就可以极大的提高运行的效率。

Main.cc的实现
#include
#include "RingQueue.hpp"
#include
using namespace std;
void *productor(void *arg)
{
// 先取出阻塞队列
RingQueue *rq = (RingQueue *)arg;
while (true)
{
int data = rq->Pop();
cout *rq = (RingQueue *)arg;
while (true)
{
int data = 10;
rq->Equeue(data);
cout *rq = new RingQueue();
// 创建生产者和消费者
pthread_t c, p;
pthread_create(&c, nullptr, consumer, rq);
pthread_create(&p, nullptr, productor, rq);
pthread_join(c, nullptr);
pthread_join(p, nullptr);
return 0;
}
2.9.一种模型两种场景总结
所以解释为什么时这两种场景?
多线程使用资源,有两种场景:
1.将目标资源整体使用【mutex+2元信号量】--队列时一整块的资源,适配的话也只能适配二元信号量也就是锁
2.将目标资源按照不同的"块”,分批使用 【信号量】-- vector数组有多块的小资源,所以可以进行信号量的分配,后并发的进行
3.线程池
3.1.线程池雏形
这段代码实现了一个基于模板的线程池,核心功能是管理一组线程并分配任务执行。以下是详细解析:
一、整体架构与核心组件
1. 类模板定义
template
class ThreadPool {
// ... 成员变量和方法 ...
};
- 模板参数 T:表示任务类型,要求 T 是可调用对象(如函数、Lambda、仿函数)。
2. 关键成员变量
vector _threads; // 存储工作线程
queue _taskq; // 任务队列
Cond _cond; // 条件变量(注:应为std::condition_variable)
mutex _mutex; // 互斥锁,保护任务队列
bool _isrunding; // 线程池运行状态
int _sleepnum; // 睡眠线程数量
- 核心设计:通过任务队列和条件变量实现线程间的工作分配与同步。
二、构造与析构逻辑
1. 构造函数
ThreadPool(int num = gnum) : _num(num), _isrunding(true) {
for (int i = 0; i < num; i++) {
_threads.emplace_back([this]() { HandlerTask(); });
}
}
- 功能:创建指定数量的工作线程(默认 5 个),每个线程执行
HandlerTask方法。 - 实现细节:
- 使用
emplace_back直接构造线程对象,避免拷贝开销。 - Lambda 表达式捕获
this指针,以便访问线程池的成员函数。
- 使用
2. 析构函数
~ThreadPool(){
Stop();
Join();
}
三、核心方法解析
1. 线程工作函数 HandlerTask
void HandlerTask() {
while (treu) {
T t;
_mutex.lock();
// 等待任务或停止信号
while (_taskq.empty() && _isrunding) {
_sleepnum++;
_cond.wait(_mutex);
_sleepnum--;
}
// 检查停止条件
if (!_isrunding && _taskq.empty()) {
break;
}
// 获取并执行任务
t = _taskq.front();
_taskq.pop();
_mutex.unlock();
t();
}
}
- 工作流程:
- 加锁检查任务队列,若为空且线程池运行中,则等待条件变量。
- 等待时记录睡眠线程数(
_sleepnum++),唤醒后减少计数。 - 若线程池停止且队列为空,退出循环。
- 否则获取任务,解锁后执行任务。
2. 任务入队 Equeue
bool Equeue(const T &t) {
if (_isrunding) {
_mutex.lock();
_taskq.push(t);
// 若所有线程都在睡眠,唤醒一个线程
if (_sleepnum == _threads.size()) {
_cond.notify_one();
}
_mutex.unlock();
return true;
}
return false;
}
- 功能:向任务队列添加任务,必要时唤醒睡眠线程。
- 唤醒策略:当所有线程都睡眠时,才唤醒一个线程,避免不必要的上下文切换。
3. 停止线程池 Stop
void Stop() {
if (!_isrunding) return;
_isrunding = false;
WakeAllThread(); // 唤醒所有睡眠线程
}
- 功能:标记线程池停止,并唤醒所有睡眠线程。
- 关键作用:确保线程能响应停止信号,避免永久阻塞。
4. 等待所有线程 Join
void Join() {
for (auto &t : _threads) {
t.join();
}
}
- 功能:阻塞等待所有线程完成任务。
- 注意事项:需要先调用
Stop,否则可能导致死锁。
四、同步机制详解
1. 条件变量与互斥锁
// 等待条件:队列为空且线程池运行中
while (_taskq.empty() && _isrunding) {
_sleepnum++;
_cond.wait(_mutex); // 释放锁并等待唤醒
_sleepnum--;
}
- 工作原理:
wait方法会释放互斥锁,使线程进入休眠状态。- 当其他线程调用
notify_one/notify_all时,线程被唤醒并重新获取锁。
- 计数作用:
_sleepnum用于记录睡眠线程数,决定是否需要唤醒线程。
2. 唤醒所有线程 WakeAllTread
void WakeAllTread() {
if (_sleepnum) {
_cond.notify_all(); // 唤醒所有睡眠线程
}
}
- 功能:当有线程睡眠时,唤醒所有等待的线程。
- 应用场景:线程池停止时,确保所有线程能响应
_isrunding的变化。
五、使用示例
#include "ThreadPool.hpp"
#include
#include
int main() {
// 创建线程池,包含3个线程
ThreadPool> pool(3);
// 提交任务
for (int i = 0; i < 10; i++) {
pool.Equeue([i]() {
std::cout << "Task " << i << " executed by thread "
<< std::this_thread::get_id() << std::endl;
});
}
// 等待一段时间
std::this_thread::sleep_for(std::chrono::seconds(2));
// 停止线程池
pool.Stop();
pool.Join();
return 0;
}
3.2.单例模式
单例模式的特点:
- 饿汉方式:吃完饭,立刻洗碗,因为下一顿吃的时候可以立刻拿着碗就能吃饭。
- 懒汉方式:吃完饭,先把碗放下,然后下一顿饭用到这个碗了再洗碗 。
常用来辅助理解单例模式里两种创建实例时机不同的设计方式,饿汉式是类加载时就创建实例(类似吃完饭立刻洗碗,提前准备好);懒汉式是在真正需要使用实例时才去创建(类似用到碗了再洗碗,延迟准备 )。
3.2.1.饿汉模式
template
class Singleton {
static T data;
public:
static T* GetInstance() {
return &data;
}
};
为什么是 “饿汉式”
在单例模式的概念里,饿汉式(Hungry Singleton)的核心特征是:单例对象在程序启动(类加载阶段)时就完成初始化,提前 “备好” ,对应到代码和类比场景:
初始化时机:
代码里的 static T data; 是静态成员变量。在 C++ 中,静态成员变量会在程序运行初期(全局作用域初始化阶段,早于 main 函数执行)就完成分配和初始化 。相当于 “吃完饭立刻洗碗”,程序一启动,单例的 data 就已经创建好、“备在那儿” 了。
3.2.2.饱汉模式
template
class Singleton {
static T* inst;
public:
static T* GetInstance() {
if (inst == NULL) {
inst = new T();
}
return inst;
}
};
为什么是懒汉(懒加载)模式
在单例模式里,“懒汉模式(Lazy Initialization)” 的核心特点是单例对象在真正被使用(首次调用获取实例方法)的时候才会进行初始化创建,对应到这段代码:
初始化时机:
代码中,静态成员指针 inst 一开始并没有实际创建 T 类型的对象(只是声明了指针 )。只有当第一次调用 GetInstance 方法时,才会检查 inst == NULL ,满足条件后才会执行 new T() 去创建单例对象。也就是 “用到的时候才去创建”,和 “吃完饭,先把碗放下,等下一顿饭用到这个碗了再洗碗” 的懒汉逻辑一致 ——延迟初始化 。
3.3.线程池的饱汉单例模式
现在我们就以上面线程池的雏形完成饱汉单例模式
1.因为单例只能创建一个类对象,所以要做下面的操作
A.将构造函数设置为私有的

B.拷贝构造和复制拷贝都给禁用

C.定义单例指针成员变量,在类外进行初始化,并提供获取单例指针的接口


2.上面只是1:n的单例饱汉模式,如何使n:n的多生产者,多消费者模式该如何实现?
A.创建静态锁,用于生产者的。

B.加上静态锁,防止多线程单例模式

浙公网安备 33010602011771号