当前位置: 首页 > news >正文

Linux--线程同步与互斥

 

目录

1. 线程互斥

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

1-2 互斥量mutex

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

2. 线程同步

2-1 条件变量

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

2-3 条件变量函数

2-4 ⽣产者消费者模型

2-4-1 为何要使⽤⽣产者消费者模型

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

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

2-5-1 BlockingQueue

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

2-7 条件变量使⽤规范

3. 线程安全和重⼊问题

4. 常⻅锁概念

4-1 死锁

4-2 死锁四个必要条件


1. 线程互斥

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

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

1-2 互斥量mutex

⼤部分情况,线程使⽤的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量
归属单个线程,其他线程⽆法获得这种变量。
但有时候,很多变量都需要在线程间共享,这样的变量称为共享变量,可以通过数据的共享,完
成线程之间的交互。
多个线程并发的操作共享变量,会带来⼀些问题。
// 操作共享变量会有问题的售票系统代码
#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, "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
为什么可能⽆法获得争取结果?
if 语句判断条件为真以后,代码可以并发的切换到其他线程
usleep 这个模拟漫⻓业务的过程,在这个漫⻓的业务过程中,可能有很多个线程会进⼊该代码
--ticket 操作本⾝就不是⼀个原⼦操作
取出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上提供的这把锁叫互斥量。
互斥量的接⼝
初始化互斥量
初始化互斥量有两种⽅法:
⽅法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:NUL
销毁互斥量
销毁互斥量需要注意:
使⽤ PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
不要销毁⼀个已经加锁的互斥量
已经销毁的互斥量,要确保后⾯不会有线程再尝试加锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
互斥量加锁和解锁
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);
返回值:成功返回0,失败返回错误号
调⽤ pthread_ lock 时,可能会遇到以下情况:
互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
发起函数调⽤时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到
互斥量,那么pthread_ lock调⽤会陷⼊阻塞(执⾏流被挂起),等待互斥量解锁。
改进上⾯的售票系统:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <pthread.h>
#include <sched.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);
// sched_yield(); 放弃CPU
} else {
pthread_mutex_unlock(&mutex);
break;
}
}
}
int main( void )
{
pthread_t t1, t2, t3, t4;
pthread_mutex_init(&mutex, NULL);
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);
pthread_mutex_destroy(&mutex);

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

经过上⾯的例⼦,⼤家已经意识到单纯的 i++ 或者 ++i 都不是原⼦的,有可能会有数据⼀致性
问题
为了实现互斥锁操作,⼤多数体系结构都提供了swap或exchange指令,该指令的作⽤是把寄存器和
内存单元的数据相交换,由于只有⼀条指令,保证了原⼦性,即使是多处理器平台,访问内存的 总线周
期也有先后,⼀个处理器上的交换指令执⾏时另⼀个处理器的交换指令只能等待总线周期。 现在
我们把lock和unlock的伪代码改⼀下

2. 线程同步

2-1 条件变量

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

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

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

2-3 条件变量函数

初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t
*restrict attr);
参数:
cond:要初始化的条件变量
attr:NULL
销毁
int pthread_cond_destroy(pthread_cond_t *cond)
等待条件满⾜
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict
mutex);
参数:
cond:要在这个条件变量上等待
mutex:互斥量,后⾯详细解释
唤醒等待
int pthread_cond_broadcast(pthread_cond_t *cond);
int pthread_cond_signal(pthread_cond_t *cond)

2-4 ⽣产者消费者模型

321原则(便于记忆)
2-4-1 为何要使⽤⽣产者消费者模型
⽣产者消费者模式就是通过⼀个容器来解决⽣产者和消费者的强耦合问题。⽣产者和消费者彼此之间不直接通讯,⽽通过阻塞队列来进⾏通讯,所以⽣产者⽣产完数据之后不⽤等待消费者处理,直接扔给阻塞队列,消费者不找⽣产者要数据,⽽是直接从阻塞队列⾥取,阻塞队列就相当于⼀个缓冲区,平衡了⽣产者和消费者的处理能⼒。这个阻塞队列就是⽤来给⽣产者和消费者解耦的。
2-4-2 ⽣产者消费者模型优点
解耦
⽀持并发
⽀持忙闲不均

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

2-5-1 BlockingQueue
在多线程编程中阻塞队列(Blocking Queue)是⼀种常⽤于实现⽣产者和消费者模型的数据结构。其与
普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放⼊了元
素;当队列满时,往队列⾥存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是
基于不同的线程来说的,线程在对阻塞队列进程操作时会被阻塞)

 

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

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

 

按照上⾯的说法,我们设计出如下的代码:先上锁,发现条件不满⾜,解锁,然后等待在条件变
量上不就⾏了,如下代码:
// 错误的设计
pthread_mutex_lock(&mutex);
while (condition_is_false) {
pthread_mutex_unlock(&mutex);
//解锁之后,等待之前,条件可能已经满⾜,信号已经发出,但是该信号可能被错过
pthread_cond_wait(&cond);
pthread_mutex_lock(&mutex);
}
pthread_mutex_unlock(&mutex);
由于解锁和等待不是原⼦操作。调⽤解锁之后, 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);
while (条件为假)
pthread_cond_wait(cond, mutex);
修改条件
pthread_mutex_unlock(&mutex);
给条件发送信号代码
pthread_mutex_lock(&mutex);
设置条件为真
pthread_cond_signal(cond);
pthread_mutex_unlock(&mutex);

3. 线程安全和重⼊问题

概念
线程安全:就是多个线程在访问共享资源时,能够正确地执⾏,不会相互⼲扰或破坏彼此的执⾏结
果。⼀般⽽⾔,多个线程并发同⼀段只有局部变量的代码时,不会出现不同的结果。但是对全局变量
或者静态变量进⾏操作,并且没有锁保护的情况下,容易出现该问题。
重⼊:同⼀个函数被不同的执⾏流调⽤,当前⼀个流程还没有执⾏完,就有其他的执⾏流再次进⼊,
我们称之为重⼊。⼀个函数在重⼊的情况下,运⾏结果不会出现任何不同或者任何问题,则该函数被
称为可重⼊函数,否则,是不可重⼊函数。
学到现在,其实我们已经能理解重⼊其实可以分为两种情况
多线程重⼊函数
信号导致⼀个执⾏流重复进⼊函数
常⻅的线程不安全的情况
不保护共享变量的函数
函数状态随着被调⽤,状态发⽣变化的函数
返回指向静态变量指针的函数
调⽤线程不安全函数的函数
常⻅不可重⼊的情况
调⽤了malloc/free函数,因为malloc函数
是⽤全局链表来管理堆的
调⽤了标准I/O库函数,标准I/O库的很多实
现都以不可重⼊的⽅式使⽤全局数据结构
可重⼊函数体内使⽤了静态的数据结构
常⻅的线程安全的情况
每个线程对全局变量或者静态变量只有读取
的权限,⽽没有写⼊的权限,⼀般来说这些
线程是安全的
类或者接⼝对于线程来说都是原⼦操作
多个线程之间的切换不会导致该接⼝的执⾏
结果存在⼆义性
常⻅可重⼊的情况
不使⽤全局变量或静态变量
不使⽤ malloc或者new开辟出的空间
不调⽤不可重⼊函数
不返回静态或全局数据,所有数据都有函数
的调⽤者提供
使⽤本地数据,或者通过制作全局数据的本
地拷⻉来保护全局数据
结论
不要被上⾯绕⼝令式的话语唬住,你只要仔细观察,其实对应概念说的都是⼀回事。

 

4. 常⻅锁概念

4-1 死锁

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

4-2 死锁四个必要条件

互斥条件:⼀个资源每次只能被⼀个执⾏流使⽤
好理解,不做解释
请求与保持条件:⼀个执⾏流因请求资源⽽阻塞时,对已获得的资源保持不放

 

不剥夺条件:⼀个执⾏流已获得的资源,在末使⽤完之前,不能强⾏剥夺

循环等待条件:若⼲执⾏流之间形成⼀种头尾相接的循环等待资源的关系

相关文章:

  • 探秘 Ruby 与 JavaScript:动态语言的多面风采
  • 最新如何在服务器中解决FFmpeg下载、安装和配置问题教程(Linux|Windows|Mac|Ubuntu)
  • 【测试】-- 测试用例
  • 基于ollama与ragflow实现本地知识库
  • 2025年认证杯数学建模竞赛A题完整分析论文(含模型、可运行代码)(共32页)
  • CSS 表格样式学习笔记
  • debian转移根目录
  • Java泛型程序设计(持续更新)
  • Kubernetes节点驱逐机制详解:保障集群稳定性的最后防线
  • 学Qt笔记
  • 计算机的运算方式
  • opencv实际应用--银行卡号识别
  • 探秘数据库连接池:HikariCP与Tomcat JDBC
  • Allpairs工具下载及操作流程(联动Deepseek)
  • Deepseek Bart模型相比Bert的优势
  • 生成与强化学习:赋予VLA系统物理行动能力
  • 定时器介绍及简单应用
  • 双Token实现用户登录身份认证-Java版
  • numpy练习
  • kubernetes常见Pod状态
  • 手机表面细菌菌落总数可能比马桶高10倍,医生详解如何洗手
  • 因港而兴,“长江黄金水道”上的宜宾故事
  • 颜福庆与顾临的争论:1930年代在中国维持一家医学院要花多少钱
  • 白天气温超30℃的北京,晚间下起了冰雹
  • 李强会见巴西总统卢拉
  • 从这些电影与影像,看到包容开放的上海