目录
2.线程间通信实现方式------最简单的方法:全局变量+锁(互斥锁)
[4.2互斥锁相关函数(pthread 库)](#4.2互斥锁相关函数(pthread 库))
[6.4.2 初始化信号量](#6.4.2 初始化信号量)
1.重要概念
①临界资源:一次仅允许一个任务(线程/进程)访问的共享资源(比如全局变量、文件、硬件设备)。是需要被保护的对象
例:唯一的坑位 / 共享的银行账户/多线程程序中的共享变量、共享文件、共享数据库连接、共享硬件(如打印机)。
②临界区(即临界代码):访问和操作临界资源的那段代码。【临界代码或临界区不可能同时被CPU任务执行】
③线程安全问题:当多个线程未受控制地并发访问同一临界资源时,导致程序结果不可预测、数据损坏或逻辑错误的问题。
比喻:争抢引发的混乱 / 账户金额出错
原因:线程的执行顺序和时机由操作系统调度,充满不确定性。
2.线程间通信实现方式------最简单的方法:全局变量+锁(互斥锁)
1.一个进程空间内部的所有线程共享数据段和堆区,所以全局变量、静态变量、堆区空间都是共享的,可以利用这些空间通信
2.多线程操作全局变量 空间时会引入资源竞争
3.多线程要避免引入资源竞争可以通过加互斥锁解决【应用层编程用互斥锁,内核编程用内旋锁】
4. 互斥的核心是排他性访问:同一时刻,只有一个线程能操作临界资源。
3.原子操作
概念:不会被CPU任务调度打断的一次最小的操作称为原子操作(即一次机器码)
4.互斥锁(Mutex)
4.1互斥锁相关概念
1.互斥概念:互斥的核心是排他性访问 ,为避免多线程资源竞争,同一宏观时刻,只有一个线程能操作临界资源。(互斥锁,则"这把锁只能一个人用")
2.互斥锁使用方法:配合资源使用,使用资源前加锁,使用资源结束后解锁
3.加锁后,无法再次加锁,必须阻塞等待解锁后才能继续加锁
4.2互斥锁相关函数(pthread 库)
相关函数【函数:就四个】【应用:就一个------避免资源竞争问题】
互斥锁的初始化 :pthread_mutex_init
互斥锁的销毁 :pthread_mutex_destroy
互斥锁的加锁 :pthread_mutex_lock
互斥锁的解锁 :pthread_mutex_unlock
pthread_mutex_init
原型:int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t*restrict attr);
功能:
互斥锁的初始化
参数:
mutex:互斥锁空间首地址
attr:互斥锁的属性,默认传NULL
返回值:
成功返回0
失败返回非0
pthread_mutex_destroy
原型:int pthread_mutex_destroy(pthread_mutex_t *mutex);
功能:
互斥锁的销毁
参数:
mutex:互斥锁空间首地址
返回值:
成功返回0
失败返回非0
pthread mutex_ lock
原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
互斥锁加锁
- 用指定的互斥锁开始加锁代码,成功则进入临界区;失败则阻塞等待。
- 注意:加锁后的代码是原子操作(线程调度不会打断这段代码)。
参数:
mutex:互斥锁空间首地址
返回值:
成功返回0
失败返回非0
pthread mutex_unlock
原型:int pthread_mutex_lock(pthread_mutex_t *mutex);
功能:
互斥锁解锁
- 将指定的互斥锁解锁,让其他线程可以竞争。解锁之后代码不再排他访问。
- 注意:加锁和解锁必须成对出现,且要在同一个线程中执行。
参数:
mutex:互斥锁空间首地址
返回值:
成功返回0
失败返回非0
4.3互斥锁使用步骤
Linux 下用 pthread_mutex_t 实现互斥锁,步骤是:定义→初始化→加锁→解锁→销毁。
4.3.1定义互斥锁
cpp
#include <pthread.h>
// 定义全局/共享的互斥锁
pthread_mutex_t lock;
4.3.2初始化互斥锁
cpp
// 示例
pthread_mutex_init(&lock, NULL);
4.3.3加锁(进入临界区)
cpp
// 示例
pthread_mutex_lock(&lock);
4.3.4解锁(离开临界区)
cpp
// 示例
pthread_mutex_unlock(&lock);
4.3.4销毁互斥锁
cpp
// 示例
pthread_mutex_destroy(&lock);
4.3.4销毁互实战案例
cpp
#include <stdio.h>
#include <stdlib.h> // 用于给指针置空
#include <string.h>
#include <pthread.h> //用于线程相关函数
int Num = 0;
pthread_mutex_t lock;
void* threadfun1(void* arg)
{
printf("线程开始执行(TID:%#x)\n",(unsigned int)pthread_self());//%#x:十六进制数,小写字母,并自动添加 0x前缀。
while(1) //死循环是为了防止进程结束,因为进程不在线程也不在
{
pthread_mutex_lock(&lock);
Num = 100;
printf("Num = %d\n",Num);
pthread_mutex_unlock(&lock);
}
return NULL;
}
void* threadfun2(void* arg)
{
printf("线程开始执行(TID:%#x)\n",(unsigned int)pthread_self());//%#x:十六进制数,小写字母,并自动添加 0x前缀。
while(1) //死循环是为了防止进程结束,因为进程不在线程也不在
{
pthread_mutex_lock(&lock);
Num = 200;
printf("Num = %d\n",Num);
pthread_mutex_unlock(&lock);
}
return NULL;
}
int main(void)
{
int ret1 = 0;
int ret2 = 0;
pthread_t tid1;
pthread_t tid2;
pthread_mutex_init(&lock,NULL); //不配置,所以置空
ret1 =pthread_create(&tid1,NULL,threadfun1,NULL);//创建线程1
ret2 =pthread_create(&tid2,NULL,threadfun2,NULL);
#if 1
if(ret1 != 0 || ret2 != 0)
{
perror("fail to pthread_create");
return -1;
}
#endif
pthread_join(tid1,NULL);//阻塞回收线程
pthread_join(tid2,NULL);
pthread_mutex_destroy(&lock);//锁的销毁
return 0;
}
注意:
①要加锁就必须各进程都加锁才能实现你想要的功能(否则你挺守规矩上厕所上锁,但是别人不守规矩一脚把门踹开)
②互斥锁只能解决资源竞争,但是谁先用谁后用不知道(互斥锁不等于同步)
③第二个人只有当第一个人解完锁,且任务调度刚好到达第二个人时,第二个人才能拿到锁
5.锁在使用时比较常见的问题------死锁
死锁的经典示例(哲学家就餐问题)
// 5个哲学家,5支筷子(5把锁) // 每个哲学家需要同时拿起左右两支筷子才能吃饭 哲学家A:拿起左筷子 → 等待右筷子(被B拿着) 哲学家B:拿起左筷子 → 等待右筷子(被C拿着) 哲学家C:拿起左筷子 → 等待右筷子(被D拿着) 哲学家D:拿起左筷子 → 等待右筷子(被E拿着) 哲学家E:拿起左筷子 → 等待右筷子(被A拿着)← 形成循环等待!
概念:死锁 是两个或更多 进程/线程 在执行过程中,因竞争共享资源 而造成的一种相互等待的状态。若无外力干涉,它们都将无法继续向前推进。
原因:多任务通信过程中由于加锁导致
死锁产生的四个充分条件:
①互斥条件(资源一次只能一个人用,我加锁1你就不能加锁1了,一个锁只能被一个人占用)
②不可剥夺条件(进程/线程已获得的资源,在未使用完之前不能被强制剥夺。不能抢别人正在用的资源)
③请求保持(拿不到锁,阻塞等待,过一段时间继续请求拿锁)
④循环等待(若干进程之间形成一种头尾相接的循环等待链资源关系。)
解决死锁(破坏充分条件即可,①②是锁的基本特性,无法破坏,只能破坏③或④):
①用pthread_mutex_trylock避免永久阻塞替代pthread_mutex_lock【作用:尝试加锁,能加就加,加不上就不加,跳过加锁这里,而去执行后边程序,防止了程序卡死】【原理:破坏"请求保持"条件】
cpp// 使用pthread_mutex_trylock避免永久阻塞 if (pthread_mutex_trylock(&lock) == 0) { // 成功获得锁 // ... 执行操作 pthread_mutex_unlock(&lock); } else { // 获取失败,执行替代逻辑或稍后重试 // 不会阻塞在这里 }②所有线程加锁顺序保持一致【最实用,能彻底预防死锁,预防优于检测】(线程1是先加锁1,再加锁2,那么线程2也要加锁1,再加锁2。否则导致死锁)
软件编码中,阻塞一定要避免(使用带延时或带尝试try的函数接口) ,因为非常容易导致卡死。或者加上超时退出
6.信号量(Semaphore)
6.1信号量相关概念
信号量的概念:信号量是一个资源,资源可以初始化、销毁、申请和释放
如果资源数>0,则申请资源是让资源数-1
如果资源数为0,申请资源时则会阻塞等待,等待有人释放资源,才能申请拿到资源
释放不会阻寨,让资源数+1
同步与异步
1.同步:拥有严格的先后执行 的逻辑顺序关际
2.异步:代码执行流程没有任何关联性
信号量应用:①信号量可以实现多线程间的同步(信号量是实现同步的工具),**让多个任务具有先后顺序关系(不仅可以防止资源竞争,还能让多任务有严格顺序,比互斥锁更强大)**②实现多功能的拆分,避免功能间耦合,一个线程完成一个概念
同步与互斥的关系:
同步是互斥的 "特例":同步不仅要排他访问,还要控制执行顺序。
互斥锁与信号量的区别
- 互斥锁:加锁和解锁是同一个线程,临界区代码短小精悍,避免休眠、大耗时的操作
- 信号量:th1 释放 th2,th2 释放 th1。由线程交叉释放。可以有适当休眠、小的耗时操作
6.2信号量的原理
信号量是一个整数 sem,通过 申请资源 和 释放资源 实现同步:
- 申请资源:sem--,若 sem<0 则线程阻塞;
- 释放资源:sem++,若 sem<=0 则唤醒一个阻塞的线程。
6.3信号量相关函数(semaphore.h库)
相关函数【函数:就四个】【应用:就一个------使多个任务具有先后顺序关系】
初始化资源 : sem_init
销毁资源 : sem_destroy
申请资源 : sem_wait
释放资源 :sem_post
sem_init
原型:int sem_init(sem_t *sem, int pshared, unsigned int value);
功能:
对信号量初始化
参数:
sem:信号量空间首地址
pshared:信号量的作用域线程间共享
0 线程间共享
非0 进程间共享
value:信号量的初始值(如 0 表示 "无资源",1 表示 "有 1 个资源")
返回值:
成功返回0
失败返回非0
sem_destory
原型:int sem_destory(sem_t *sem);
功能:
使用完毕将指定的信号量销毁。
参数:
sem:信号量空间首地址
返回值:
成功返回0
失败返回非0
sem_wait
原型:int sem_wait(sem_t *sem);
功能:
申请资源,让资源数-1,如果资源数为0,则阻塞等待, 一旦有资源则自动 申请资源并继续运行程序。
参数:
sem:信号量空间首地址
返回值:
成功返回0
失败返回非0
sem_post
原型:int sem_post(sem_t *sem);
功能:
释放资源,将指定的 sem 信号量资源释放,让资源数+1(即执行 sem = sem+1),线程在该函数上不会阻塞。
参数:
sem:信号量空间首地址
返回值:
成功返回0
失败返回非0
6.4信号量的使用步骤
步骤是:定义→初始化→PV 操作→销毁。
6.4.1定义信号量
cpp
#include <semaphore.h>
sem_t sem_r;
sem_t sem_w;
6.4.2 初始化信号量
cpp
sem_init(&sem_r, 0, 0);
sem_init(&sem_w, 0, 1);
6.4.3信号量的操作------申请资源
cpp
sem_wait(&sem_w);
6.4.4信号量的操作------释放资源
cpp
sem_post(&sem_r);
6.4.5销毁信号量
cpp
sem_destroy(&sem_r);
sem_destroy(&sem_w);
6.4.6信号量同步实战示例
cpp
#include <stdio.h>
#include <stdlib.h> // 用于给指针置空
#include <string.h>
#include <pthread.h> //用于线程相关函数
#include <semaphore.h> //用于信号量相关函数
char tmpbuffer[4096] = {0};
sem_t sem_r; //读资源
sem_t sem_w; //写资源(写进tmpbuffer)
void* threadfun1(void* arg) //写
{
printf("线程开始执行(TID:%#x)\n",(unsigned int)pthread_self());//%#x:十六进制数,小写字母,并自动添加 0x前缀。
while(1) //死循环是为了防止进程结束,因为进程不在线程也不在
{
sem_wait(&sem_w);
gets(tmpbuffer);
sem_post(&sem_r);
if(0 == strcmp(tmpbuffer,".quit"))
{
break;
}
}
return NULL;
}
void* threadfun2(void* arg) //读出来
{
printf("线程开始执行(TID:%#x)\n",(unsigned int)pthread_self());//%#x:十六进制数,小写字母,并自动添加 0x前缀。
while(1) //死循环是为了防止进程结束,因为进程不在线程也不在
{
sem_wait(&sem_r);
if(0 == strcmp(tmpbuffer,".quit"))
{
break;
}
printf("tmpbuff = %s\n",tmpbuffer);
sem_post(&sem_w);
}
return NULL;
}
int main(void)
{
int ret1 = 0;
int ret2 = 0;
pthread_t tid1;//创建线程
pthread_t tid2;
sem_init(&sem_r,0,1);
sem_init(&sem_w,0,0);
ret1 =pthread_create(&tid1,NULL,threadfun1,NULL);//创建线程1
ret2 =pthread_create(&tid2,NULL,threadfun2,NULL);
#if 1
if(ret1 != 0 || ret2 != 0)
{
perror("fail to pthread_create");
return -1;
}
#endif
pthread_join(tid1,NULL);//阻塞回收线程
pthread_join(tid2,NULL);
sem_destroy(&sem_r);//信号量的销毁
sem_destroy(&sem_w);//信号量的销毁
return 0;
}
