云计算百科
云计算领域专业知识百科平台

Linux:线程间通信

目录

1.重要概念

2.线程间通信实现方式——最简单的方法:全局变量+锁(互斥锁)

3.原子操作

4.互斥锁(Mutex)

4.1互斥锁相关概念

4.2互斥锁相关函数(pthread 库)

4.3互斥锁使用步骤

4.3.1定义互斥锁

4.3.2初始化互斥锁

4.3.3加锁(进入临界区)

4.3.4解锁(离开临界区)

4.3.4销毁互斥锁

4.3.4销毁互实战案例

5.锁在使用时比较常见的问题——死锁

6.信号量(Semaphore)

6.1信号量相关概念

6.2信号量的原理

6.3信号量相关函数(semaphore.h库)

6.4信号量的使用步骤

6.4.1定义信号量

6.4.2 初始化信号量

6.4.3信号量的操作——申请资源

6.4.4信号量的操作——释放资源

6.4.5销毁信号量

6.4.6信号量同步实战示例


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定义互斥锁

#include <pthread.h>
// 定义全局/共享的互斥锁
pthread_mutex_t lock;

4.3.2初始化互斥锁

// 示例
pthread_mutex_init(&lock, NULL);

4.3.3加锁(进入临界区)

// 示例
pthread_mutex_lock(&lock);

4.3.4解锁(离开临界区)

// 示例
pthread_mutex_unlock(&lock);

4.3.4销毁互斥锁

// 示例
pthread_mutex_destroy(&lock);

4.3.4销毁互实战案例

#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【作用:尝试加锁,能加就加,加不上就不加,跳过加锁这里,而去执行后边程序,防止了程序卡死】【原理:破坏“请求保持”条件】

// 使用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定义信号量

#include <semaphore.h>
sem_t sem_r;
sem_t sem_w;

6.4.2 初始化信号量

sem_init(&sem_r, 0, 0);
sem_init(&sem_w, 0, 1);

6.4.3信号量的操作——申请资源

sem_wait(&sem_w);

6.4.4信号量的操作——释放资源

sem_post(&sem_r);

6.4.5销毁信号量

sem_destroy(&sem_r);
sem_destroy(&sem_w);

6.4.6信号量同步实战示例

#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;
}

赞(0)
未经允许不得转载:网硕互联帮助中心 » Linux:线程间通信
分享到: 更多 (0)

评论 抢沙发

评论前必须登录!