news 2026/10/19 2:11:46

Linux ---- 线程同步与互斥

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux ---- 线程同步与互斥

1 线程互斥

1-1进程线程间的互斥相关背景概念

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

1-2 线程为什么要互斥

线程之间是相互共享地址空间的,所以线程会共享大部分资源---公共资源,这就会导致可能几个线程同一时间都会访问一个公共资源,这会导致数据不一致问题,所以要引入线程互斥与同步(这个我们之后再讨论)

1-3 重谈信号量

之前我们在进程间通信的时候谈过信号量,但是谈的很草率,现在就在线程部分好好重新讲解一下信号量

1-3-1 信号量是什么

可以理解为信号灯,本质是一个计数器,用来表示临界资源中,资源数量的多少

1-3-2 理解信号量

平常我们看电影需要购买电影票,购买电影票的时候需要选取位置,当位置选完后这个位置就标识有人不可以再被别人选走了。

电影院的某个放映厅就是一个临界资源(内部包含了很多子资源)。

电影院最怕两个问题:

1 票卖多了

2 票号重复

其中,买票:本质就是对于资源的预定,解决票卖多的问题。而信号量就像是这个座位的计数器,描述的是这个这个电影院还剩下多少作为,什么座位可以坐什么座位有人预定了,解决票号重复的问题。

所以信号量这就是个计数器,描述的是临界资源中,资源数量的多少。所有进程/线程,访问临界资源中的一小块,就必须申请信号量。进程访问资源前,先申请信号量,本质就是对于资源的预订机制(资源给你了等你随时访问没人和你抢)

细节1 :

信号量本身就是共享资源,所以申请和释放信号量也要是原子的。申请就是--计数器 ->P操作。释放信号量就是++技术器 -> V操作。通过对于信号量的PV操作来完成资源的预定机制。

细节2:

信号量只有0或者1两态的信号量称为二元信号量,否则就是多元信号量

当信号量这个计数器已经被用完时,则操作信号量的进程/线程会被阻塞挂起。

在操作系统中,信号量也是要被管理的,所以先描述再组织。上述理解其实我们就可以猜测到信号量的结构了

struct sem { // int count ---计时器 // 锁 --- 用来保证原子性操作 // waitqueue --- 阻塞挂起的等待队列 };

1-4 互斥量mutex

大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个线程,其他线程无法获得这种变量。 但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完成线程之间的交互。 多个线程并发的操作共享变量,会带来⼀些问题。

例如下面售票的代码:

#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.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); } ⼀次执⾏结果: 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 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
--ticket 操作本身就不是一个原子操作,因为 --ticket其实会分为三个步骤

当其中一个线程正在执行这个三步分别的汇编代码时候,有可能会被其他线程打断

多线程中,usleep 制造了更多的并发,更多的切换 -> 选择新的线程,从内核态,返回用户态的时候进行检查是否要切换。
所以切换的时间点是什么时候?
1 时间片耗尽
2 阻塞式IO
3 sleep等..... (切走)
上述的三种情况,线程都会陷入内核中,所以是从内核态返回用户态的时候进行检查
取出ticket--部分的汇编代码 objdump -d a.out > test.objdump 152 40064b: 8b 05 e3 04 20 00 mov 0x2004e3(%rip),%eax # 600b34 <ticket> 153 400651: 83 e8 01 sub $0x1,%eax 154 400654: 89 05 da 04 20 00 mov %eax,0x2004da(%rip) # 600b34 <ticket>
-- 操作并不是原子操作,而是对应三条汇编指令:
load :将共享变量ticket从内存加载到寄存器中
update : 更新寄存器里面的值,执行-1操作
store :将新值,从寄存器写回共享变量ticket的内存地址
要解决以上问题,需要做到三点:
代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许⼀个线程进入该临界区。
如果线程不在临界区中执行,那么该线程不能阻止其他线程进⼊临界区。
要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。
注意:
C语言转化后的汇编代码,每一条汇编代码都是原子的。

1-5 互斥量的接口

1-5-1 初始化互斥量

初始化互斥量有两种方法:
方法1,静态分配:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER//设置在全局域中

方法2,动态分配:

int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr); 参数: mutex:要初始化的互斥量 attr:输入对对应互斥量的属性设置,一般使用默认的传NULL就行 返回值: 成功返回0 不成功返回非0

1-5-2 销毁互斥量

销毁互斥量需要注意:
使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁一个已经加锁的互斥量
已经销毁的互斥量,要确保后面不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex); 参数: mutex:要销毁的互斥量 返回值: 成功返回0 不成功返回非0

1-5-3 互斥量的加锁和解锁

int pthread_mutex_lock(pthread_mutex_t *mutex); 参数: mutex:要加锁的互斥量 返回值: 成功返回0,失败返回错误号 int pthread_mutex_trylock(pthread_mutex_t *mutex);//加锁失败不会阻塞 参数: mutex:要加锁的互斥量 返回值: 成功返回0,失败返回错误号 int pthread_mutex_unlock(pthread_mutex_t *mutex); 参数: mutex:要解锁的互斥量 返回值: 成功返回0,失败返回错误号
调用 pthread_ lock 就是竞争申请锁,多线程都得看到锁,锁本身就是临界资源!!
所以申请锁的过程必须是原子的
所以调用 pthread_ lock,可能会遇到以下情况:
1 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功,继续向后运行访问临界区的代码,访问临界资源
2 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_ lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。
锁提供的能力的本质:执行临界区代码又并行转化为串行,在我调用期间不会被打扰,也是一种变相的原子性的表现
调用 pthread_mutex_trylock 则失败不会被挂起而是返回 EBUSY,不阻塞。不会进临界区,线程继续往下执行 trylock 后面的代码,线程不会结束!
改进上面的售票系统:
#include <pthread.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> int ticket = 100; pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; 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_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); }

我们先对上述代码中进行区域划分

从上图中可以看出,临界区是对临界资源的一种保护,其本质就是用锁对临界区代码进行保护

如果有一个线程,不遵守你的规则呢?那是你在写bug,所有线程都必须遵守

加锁之后,在临界区内部,允许线程切换吗?切换了会怎么样?是允许切换的,因为我当前进程,并没有释放锁,我是锁的持有者的身份被切换的,即使我不在了,其他线程也得等我回来执行完代码,释放锁,其他线程才能展开锁的竞争,从而进入临界区。所以切换了页不会怎么样,所有线程都必须等我跑完

举例理解:

今天有一个超级自习室,每个人都想进入,但是这个自习室规定了一次只能进一个人。所以当第一个到的人就会将墙上的钥匙拿走,所以就算他离开自习室出去吃饭的时候,他只要带上钥匙,那么其他人也还是无法进入这个自习室。那么站在外边的人是如何看待你自习的过程的?要么不用,要么用完,对外边的人才有意义。所以这体现了原子性,你的自习,对于外边的人来说是原子的!!

1-6互斥量实现原理探究

1-6-1锁的原理

1 硬件级实现:关闭时钟中断,则线程不会发生切换(我们这里不详细描述)

2 软件级实现:为了实现互斥锁操作,大多数体系结构都提供了swap或者exchange指令(只有一条指令保证原子性),该指令的作用是把寄存器和内存单元的数据相交互。

即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时另⼀个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码改一下

锁的原理本质就是在内存中有一个非0的数。

如果今天有两个线程A和B竞争锁,当他们同时执行lock时。先将对应存放mutex的寄存器清零,然后再执行交换内存中锁的值的。因为指令是原子性,所以只会保证只有一个线程会拿到这个值,所以只会有一个线程进入临界区。而且因为这个数据会保存在执行流硬件上下文,所以可以保证切换其他进程拿不到“锁”,进入不了临界区。

1-6-2 理解锁的原理

进程/线程切换:CPU内的寄存器硬件只有一套(理解锁的前提),但CPU寄存器内的数据可以有多份,各自一份,存储在当前执行流的上下文

换句话说,如果把一个变量的内容,交换到CPU寄存器内部,本质是:把该变量的内容,获取到当前执行流的硬件上下文,当前CPU寄存器的硬件上下文(其实就是各个寄存器的内容),属于:进程/线程私有的!!

我们用swap,exchange将内存中的变量,交换到CPU的寄存器中:本质是当前线程/进程,在获取锁,因为是交换,不是拷贝!!所以只有一份,所以,谁申请到,谁持有锁!!

2 线程同步

2-1 初步理解线程同步

引入新的技术,必定引入新的问题,为了进一步解决问题,就必须有新的技术别引入。而线程的同步的引入就是为了解决线程互斥引入锁引发的问题。

在我们谈互斥的时候曾经谈过一个自习室的例子,你思考一下如果这个人一个人已经拿到锁了,现在准备出来,但是他不走,故意将钥匙放回,但是因为他刚将钥匙放回,所以他理论上比任何一个人都要距离这个钥匙都近,此时他决定再申请钥匙就又可以拿到了。如此反复的话。其他人就不可能得到钥匙。这就会导致线程饥饿问题。

但是这样纯互斥,有错吗?没错!但是会导致不太高效,所以引入了线程同步。、

那线程同步到底是什么?再次以上述例子为例。如果这个时候引入一个自习室管理员。他管理这个自习室,并且定下了两个规则。其一,不能立即二次申请,其二,外边的人,进行排队,退出的人,必须跑到队列的尾部,进行二次申请

这样再保证自习室安全的前提下,让所有执行流,访问临界资源,按照一定的顺序进行访问资源,这就是线程同步

2-2 同步概念与竞态条件

同步:
在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免
饥饿问题,叫做同步
竞态条件:
因为时序问题,而导致程序异常,我们称之为竞态条件。在线程场景下,这种问题也不难理解

2-3 条件变量

2-3-1 为什么要条件变量

当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量。
条件变量是实现线程同步的一种方式

2-3-2 条件变量函数

初始化

静态分配:

pthread_cond_t gond =PTHREAD_COND_INITIALIZER

动态分配:

int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr); 参数: cond:要初始化的条件变量 attr:NULL 返回值: 成功返回0,失败返回非0

销毁

int pthread_cond_destroy(pthread_cond_t *cond) 参数: cond:要初始化的条件变量 返回值: 成功返回0,失败返回非0

使用 PTHREAD_COND_INITIALIZER 初始化的互斥量不需要销毁

等待条件满足

int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex); 参数: cond:要在这个条件变量上等待 mutex:互斥量,后⾯详细解释 返回值: 成功返回0,失败返回非0

等待是什么条件才会等?等待之前就要对资源数量进行判断。判定本身就是访问临界资源!所以判定一定是在临界区内部的。判断的结果,也一定是在临界资源内部。所以,条件不满足要休眠,一定是在临界区休眠的。所以当这个等待的线程被唤醒,也是要先竞争锁成功才会从pthread_cond_wait 成功返回。

证明一件事:条件变量,可以允许线程等待可以允许一个线程唤醒在cond等待的其他线程,实现同步过程

pthread_cond_wait调用成功,挂起当前线程之前,要先自动释放锁

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond); 参数: cond:要在这个条件变量上等待的线程全部唤醒 返回值: 成功返回0,失败返回非0 int pthread_cond_signal(pthread_cond_t *cond); 参数: cond:要在这个条件变量上等待的一个线程唤醒 返回值: 成功返回0,失败返回非0

2-3-3 理解条件变量

举例理解:

今天有一个人往一个盘子放苹果,然后有多个人想要得到这个苹果。为了保证在放苹果同时又有人在拿苹果导致错乱,所以给这个盘子加上锁。但是,如果放苹果的人放的慢,拿苹果的人拿的很快,就会导致拿苹果的人一直访问锁,访问临界资源,这会导致效率极低。所以,放苹果的人就设置了一个铃铛,在没有放苹果的时候,让拿苹果的人去等待队列里面等,当有苹果的时候用铃铛通知他们,这样就可以提高效率,而而让拿苹果的人访问有一定的顺序性,所以实现了线程同步。而这个铃铛通知机制,和队列等待机制统称为条件变量

2-4 生产者消费者模型

321原则(便于记忆) --- 三种关系,两个角色,一个交易场所

2-4-1为何要使用生产者消费者模型

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

2-4-2 生产者消费者模型优点

1 解耦
2 支持并发
3 支持忙闲不均
4 提高效率,但是实际上生产者和消费者是互斥的,所以数据放入和数据拿去并不高效,那高效在那?高效在于,生产者获取任务和消费者消费任务是可以并发执行的

三种要素:

生产者之间,竞争关系,互斥关系

消费者和消费者之间:互斥关系

生产者和消费者之间:互斥 和 同步

两个角色:

生产者角色和消费者角色(线程承担)

一个交易场所:

以特定结构构成的一块“内存”空间

三种要素,生产者,消费者,一个消费场所,这些都是临界资源。

2-5 基于BlockingQueue的生产者消费者模型

2-5-1 BlockingQueue

在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)
总的来说,阻塞队列是一个容量具有上线的队列,不满足读写条件的时候就也好进行阻塞对应的线程

2-5-2 C++ queue模拟阻塞队列的生产消费模型

BlockQueue.hpp:
#ifndef __BLOCK_QUEUE_HPP__ #define __BLOCK_QUEUE_HPP__ #include <iostream> #include <string> #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); } void Enqueue(T &in) // ⽣产者⽤的接⼝ { pthread_mutex_lock(&_mutex); while(IsFull()) // 保证代码的健壮性 { // ⽣产线程去等待,是在临界区中休眠的!你现在还持有锁呢!!! // 1. pthread_cond_wait调⽤是: a. 让调⽤线程等待 b. ⾃动释放曾经持有的 //_mutex锁 c. 当条件满⾜,线程唤醒,pthread_cond_wait要求线性 // 必须重新竞争_mutex锁,竞争成功,⽅可返回!!! // 之前:安全 _productor_wait_num++; pthread_cond_wait(&_product_cond, &_mutex); // 只要等待,必定会有 //唤醒,唤醒的时候,就要继续从这个位置向下运⾏!! _productor_wait_num--; // 之后:安全 } // 进⾏⽣产 // _block_queue.push(std::move(in)); // std::cout << in << std::endl; _block_queue.push(in); // 通知消费者来消费 if(_consumer_wait_num > 0) pthread_cond_signal(&_consum_cond); // pthread_cond_broadcast pthread_mutex_unlock(&_mutex); } void Pop(T *out) // 消费者⽤的接⼝ --- 5个消费者 { pthread_mutex_lock(&_mutex); while(IsEmpty()) // 保证代码的健壮性 { // 消费线程去等待,是在临界区中休眠的!你现在还持有锁呢!!! // 1. pthread_cond_wait调⽤是: a. 让调⽤进程等待 b. ⾃动释放曾经持有的 //_mutex锁 _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); // pthread_cond_signal(&_product_cond); } ~BlockQueue() { pthread_mutex_destroy(&_mutex); pthread_cond_destroy(&_product_cond); pthread_cond_destroy(&_consum_cond); } private: std::queue<T> _block_queue; // 阻塞队列,是被整体使⽤的!!! int _cap; // 总上限 pthread_mutex_t _mutex; // 保护_block_queue的锁 pthread_cond_t _product_cond; // 专⻔给⽣产者提供的条件变量 pthread_cond_t _consum_cond; // 专⻔给消费者提供的条件变量 int _productor_wait_num; int _consumer_wait_num; };
注意:
这里采用模版,是想告诉我们,队列中不仅仅可以防止内置类型,比如int, 对象也可
以作为任务来参与生产消费的过程哦.
pthread_cond_wait 调用成功,挂起当前线程之前,要先自动释放锁
当线程被唤醒的时候,默认就在临界区唤醒!要从pthread_cond_wait成功返回,需要当前新程,重新申请锁
如果我被唤醒,但是锁申请失败了??我就就会在锁上阻塞等待
pthread_cond_wait 有可能出现伪唤醒,即如果多生产单消费就可能出现只有一个空间,但是多个生产者被唤醒,导致出现bug,将条件变量判断从if语句改为while语句就可以解决这个问题

2-6 为什么pthread_cond_wait需要互斥量?

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有⼀个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化。所以一定要用互斥锁来保护。没有互斥锁就无法安全的获取和修改共享数据。

按照上面的说法,我们设计出如下的代码:先上锁,发现条件不满足,解锁,然后等待在条件变量上不就行了,如下代码:
// 错误的设计 pthread_mutex_lock(&mutex); while (condition_is_false) { pthread_mutex_unlock(&mutex); //解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过 pthread_cond_wait(&cond, &mutex); pthread_mutex_lock(&mutex); } pthread_mutex_unlock(&mutex);
由于解锁和等待不是原子u操作。调用解锁之后, pthread_cond_wait 之前,如果已经有其他
线程获取到互斥量,摒弃条件满足,发送了信号,那么 pthread_cond_wait 将错过这个信号,可能会导致线程永远阻塞在这个 pthread_cond_wait 。所以解锁和等待必须是一个原子
操作。
int pthread_cond_wait(pthread_cond_ t *cond,pthread_mutex_ t * mutex); 进入该函数后,会去看条件量等于0不?等于,就把互斥量变成1,直到cond_ wait返回,把条件量改成1,把互斥量恢复成原样。

2-7条件变量使用规范

等待条件代码
pthread_mutex_lock(&mutex); 设置条件为真 pthread_cond_signal(cond); pthread_mutex_unlock(&mutex);

给条件发送信号代码

pthread_mutex_lock(&mutex); while (条件为假) pthread_cond_wait(cond, mutex); 修改条件 pthread_mutex_unlock(&mutex);

2-8 POSIX信号量

2-8-1 回顾信号量

POSIX信号量和上面谈的信号量是一样,本质都是一个计数器,都是对于资源的预订机制。指示在接口实现上用了POSIX的方案风格

多线程使用资源,有两种情况:

1 将目标资源整体使用 (mutex+2元信号量)

2 将目标资源按照不同的块,分批使用(信号量)

所有的线程都要看到sem,计数器,sem++,sem--。信号量本质也是临界资源

2-8-2POSIX信号量接口

初始化信号量:

#include <semaphore.h> int sem_init(sem_t *sem, int pshared, unsigned int value); 参数: sem:输出型参数,传入要初始化的信号量 pshared:0表⽰线程间共享,⾮零表⽰进程间共享 value:信号量初始值 返回值: 成功返回0,失败返回非0

销毁信号量

int sem_destroy(sem_t *sem); 参数: sem:表示要销毁的信号量 返回值: 成功返回0,失败返回非0

等待信号量

功能:等待信号量,会将信号量的值减1 int sem_wait(sem_t *sem); //P() 参数: sem:输入型参数,传入要操作的信号量 返回值: 成功返回0,失败返回非0

发布信号量

功能:发布信号量,表⽰资源使⽤完毕,可以归还资源了。将信号量值加1。 int sem_post(sem_t *sem);//V() 参数: sem:输入型参数,传入要操作的信号量 返回值: 成功返回0,失败返回非0

2-8-3 基于环形队列的生产消费模型

环形队列采用数组模拟,用模运算来模拟环状特性

环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预留⼀个空的位置,作为满的状态
但是我们现在有信号量这个计数器,就很简单的进行多线程间的同步过程
#pragma once #include <iostream> #include <semaphore.h> // 随⼿做⼀下封装 class Sem { public: Sem(int n) { sem_init(&_sem, 0, n); } void P() { sem_wait(&_sem); } void V() { sem_post(&_sem); } ~Sem() { sem_destroy(&_sem); } private: sem_t _sem; }; #pragma once #include <iostream> #include <string> #include <vector> #include <semaphore.h> #include <pthread.h> // 单⽣产,单消费 // 多⽣产,多消费 // "321": // 3: 三种关系 // a: ⽣产和消费互斥和同步 // b: ⽣产者之间: // c: 消费者之间: // 解决⽅案:加锁 // 1. 需要⼏把锁?2把 // 2. 如何加锁? 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); } 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(); } ~RingQueue() { pthread_mutex_destroy(&_productor_mutex); pthread_mutex_destroy(&_consumer_mutex); } private: // 1. 环形队列 std::vector<T> _ring_queue; int _cap; // 环形队列的容量上限 // 2. ⽣产和消费的下标 int _productor_step; int _consumer_step; // 3. 定义信号量 Sem _room_sem; // ⽣产者关⼼ Sem _data_sem; // 消费者关⼼ // 4. 定义锁,维护多⽣产多消费之间的互斥关系 pthread_mutex_t _productor_mutex; pthread_mutex_t _consumer_mutex; };

实现这个生产消费者模型要遵守4个约定。

1 队列空的时候,生产者现运行

2 队列满的时候,消费者先运行

3 生产者不能把消费者套两个圈以上

4 消费者不能超过生产者

只要我们不访问同一个位置的时候,我们就可以同时访问。

什么时候,我们会在同一个位置上?为空或者为满的时候!!(如果不为空或者不为满同时运行)

为空:只能[互斥]生产者先[同步]运行

为满:只能[互斥]消费者先[同步]运行

结论:

1 环形队列,不为空 && 不为满,生产消费可以同时进行

2 环形队列,为空||为满,生产和消费,需要同步互斥

如何保证实现这些约定的?这就更信号量的机制有关,事项划分资源,当想要访问资源都会检查信号量这个计数器判断是否合法

注意访问同一块空间的时候操作都是原子的

2-8-4 再次理解信号量

进一步理解信号量:

信号量的另一个本质:信号量把对临界资源是否存在?是否就绪?等条件,以原子的形式,呈现在访问临界资源之前就判断了!!

如果资源可以拆分,你可以考虑sem。

如果对于资源是整体使用的,你就使用mutex

3线程安全和重入问题

3-1 概念

线程安全:
就是多个线程在访问共享资源时,能够正确地执行,不会相互干扰或破坏彼此的执行结
果。一般而言,多个线程并发同一段只有局部变量的代码时,不会出现不同的结果。但是对全局变量或者静态变量进行操作,并且没有锁保护的情况下,容易出现该问题。
线程安全描述的是线程本身健康或者安全状态
重入:
同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。、一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数。
重入描述的是函数的特征
其实我们已经能理解重入其实可以分为两种情况
1 多线程重入函数
2 信号导致一个执行流重复进入函数
常见的线程不安全的情况
•不保护共享变量的函数
•函数状态随着被调用,状态发生变化的函数
•返回指向静态变量指针的函数
•调用线程不安全函数的函数
常见不可重入的情况
•调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
•调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
•可重入函数体内使用了静态的数据结构
常见的线程安全的情况
•每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,⼀般来说这些线程是安全的
•类或者接口对于线程来说都是原子操作
•多个线程之间的切换不会导致该接口的执行结果存在⼆义性
常见可重入的情况
•不使用全局变量或静态变量
•不调用不可重入函数
•不返回静态或全局数据,所有数据都有函数的调用者提供
•使用本地数据,或者通过制作全局数据的本地拷贝来保护全数据

3-2 结论

可重入与线程安全联系
•函数是可重入的,那就是线程安全的(其实知道这⼀句话就够了)
•函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
•如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
可重入与线程安全区别
可重入函数是线程安全函数的一种
线程安全不一定是可重入的,而可重⼊函数则一定是线程安全的。
如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还
未释放则会产生死锁,因此是不可重入的。
注意:
如果不考虑信号导致一个执行流重复进入函数这种重入情况,线程安全和重入在安全角
度不做区分
但是线程安全侧重说明线程访问公共资源的安全情况,表现的是并发线程的特点
可重入描述的是一个函数是否能被重复进入,表示的是函数的特点

4常见锁概念

4-1 死锁

死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的⼀种永久等待状态。
为了方便表述,假设现在线程A,线程B必须同时持有锁1和锁2,才能进行后续资源的访问
申请一把锁是原子的,但是申请两把锁就不一定了

造成的结果是

4-2 死锁四个必要条件

互斥条件:
一个资源每次只能被一个执行流使用
请求与保持条件:
一个执行流因请求资源而阻塞时,对已获得的资源保持不放
不剥夺条件:
一个执行流已获得的资源,在末使用完之前,不能强行剥夺
循环等待条件:
若干执行流之间形成一种头尾相接的循环等待资源的关系

5STL,智能指针和线程安全

5-1 STL中的容器是否是线程安全的?

不是.原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

5-2 智能指针是否是线程安全的?

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原⼦操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 2:11:32

Agent 的中断与恢复:跑到一半能停下来吗

个人主页&#xff1a;> for_ever_love__ <&#xff08;欢迎各位大佬莅临&#x1f60a;&#xff09; 其他栏目: > 大模型开发从0到1 < 其他栏目: > iOS项目总结大全 < 其他栏目: > 我想学python了 < 其他栏目: > iOS UI < 文章目录Agent 的中断与…

作者头像 李华
网站建设 2026/10/19 2:11:30

快速排序(力扣基础)(递归)

核心目标&#xff1a;在无序数组中&#xff0c;不用完全排序&#xff0c;找到第 k 大 / 第 k 小元素 推荐&#xff1a;数据结构合集 - 快速排序(算法过程, 效率分析, 稳定性分析)_哔哩哔哩_bilibili int quickselect(int[] nums, int l, int r, int k) {if (l r) return num…

作者头像 李华
网站建设 2026/10/19 2:11:28

基于TRL库的SFT微调实现

一.TRL库的介绍 TRL库提供一系列的Trainer&#xff0c;实现大语言模型的微调和对齐训练。 使用TRL进行SFT微调的时候&#xff0c;需要先配置SFTTrainer实例。SFTTrainer包含了所需要训练的model,tokenizer以及SFTConfig参数。配置完成之后&#xff0c;即可调用SFTTrainer实例…

作者头像 李华
网站建设 2026/10/19 2:10:49

Flash 缓存没刷新读旧值——写了缓存未回写,写后 invalidate/回写

Flash 缓存没刷新读旧值——写了缓存未回写&#xff0c;写后 invalidate/回写文章标签&#xff1a;#嵌入式开发 #单片机 #Flash #OTA升级 #STM32 #扇区 #擦写 #缓存 #回写一、开篇&#xff1a;那个"写进去了却读不出"的缓存 做 STM32F7/H7&#xff08;带 DCache/ICac…

作者头像 李华
网站建设 2026/10/19 2:10:10

联调时我当了太久人肉快递员,最后让两个 agent 自己聊上了

开发最耗人的地方&#xff0c;我一直觉得不是写代码。 前几天有个活&#xff1a;一部分功能跑在无影云上&#xff0c;手机插在本地这台 PC 上。做起来是这样——云上跑一遍&#xff0c;看日志&#xff0c;复制结论&#xff1b;切到本地&#xff0c;接上手机&#xff0c;再跑一遍…

作者头像 李华
网站建设 2026/10/19 2:10:07

处理超时未支付订单:两套轻量化方案,附 PHP 极简实现

电商交易类业务&#xff0c;「待支付订单超时自动取消」是个绕不开的需求。用户占着库存、优惠券、活动名额迟迟不付款&#xff0c;不及时释放就是实打实的资损。很多技术人第一反应就是上延迟队列、分布式事务、各种中间件&#xff0c;好像不用点 “高大上” 的技术就不算做架…

作者头像 李华