Linux:线程间通信

目录

1.重要概念

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

3.原子操作

4.互斥锁(Mutex)

4.1互斥锁相关概念

[4.2互斥锁相关函数(pthread 库)](#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.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定义互斥锁

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;
}
相关推荐
一次旅行1 小时前
Linux安全总结
linux·运维·安全
木尧大兄弟1 小时前
Ubuntu 系统安装 OpenClaw 并接入飞书记录
linux·ubuntu·飞书·openclaw
liliangcsdn1 小时前
深入探索TD3算法的推理过程
开发语言·php
菜鸡儿齐1 小时前
leetcode-有效的括号
linux·算法·leetcode
谁刺我心1 小时前
qt源码、qt在线安装器镜像下载
开发语言·qt
jllllyuz1 小时前
实际气体状态方程:Peng-Robinson(P-R)方程计算指南
开发语言·matlab
LYS_06182 小时前
C++学习(8)(文件输入输出,类和对象(1))
开发语言·c++·学习
历程里程碑2 小时前
26信号处理一:从闹钟到进程控制的奥秘
linux·运维·服务器·开发语言·c++·算法·排序算法
jghhh012 小时前
基于C# WinForm实现自动在线升级的方案
开发语言·c#