Linux 线程同步、互斥锁、避免死锁、条件变量

news2024/11/27 13:42:09

1. 线程同步概述

线程同步定义

线程同步,指的是控制多线程间的相对执行顺序,从而在线程间正确、有序地共享数据,以下为线程同步常见使用场合。

  • 多线程执行的任务在顺序上存在依赖关系
  • 线程间共享数据只能同时被一个线程使用

线程同步方法

在实际项目中,经常使用的线程同步方法主要分为三种:

  • 互斥锁
  • 条件变量
  • Posix信号量(包括有名信号量和无名信号量)

本节内容只介绍互斥锁和条件变量,Posix信号量后续在Posix IPC专题中介绍。

2. 互斥锁

互斥锁概念

互斥锁用于确保同一时间只有一个线程访问共享数据,使用方法为:

  • 加锁
  • 访问共享数据
  • 解锁

对互斥锁加锁后,任何其他试图再次对其加锁的线程都会被阻塞,直到当前线程释放该互斥锁,解锁时所有阻塞线程都会变成可运行状态,但究竟哪个先运行,这一点是不确定的。

互斥锁基本API

初始化与销毁

互斥锁是用pthread_mutex_t数据类型表示的,在使用互斥锁之前,需要先进行初始化,初始化方法有两种:

  • 设置为常量PTHREAD_MUTEX_INITIALIZER,只适用于静态分配的互斥锁
  • 调用pthread_mutex_init函数,静态分配和动态分配的互斥锁都可以

互斥锁使用完以后,可以调用pthread_mutex_destroy进行销毁,尤其是对于动态分配的互斥锁,在释放内存前,调用pthread_mutex_destroy是必须的。

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

//两个函数的返回值:成功返回0,失败返回错误编号
int pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *attr);
int pthread_mutex_destroy(pthread_mutex_t *mutex);

其中,pthread_mutex_init的第二个参数attr用于设置互斥锁的属性,如果要使用默认属性,只需把attr设为NULL。

上锁与解锁

//两个函数的返回值:成功返回0,失败返回错误编号
int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

对互斥锁上锁,需要调用pthread_mutex_lock,如果互斥锁已经上锁,调用线程将阻塞到该互斥锁被释放。
对互斥锁解锁,需要调用pthread_mutex_unlock

两个特殊的上锁函数

尝试上锁

//成功返回0,失败返回错误编号
int pthread_mutex_trylock(pthread_mutex_t *mutex);

如果不希望调用线程阻塞,可以使用pthread_mutex_trylock尝试上锁:

  • 若mutex未上锁,pthread_mutex_trylock将加锁成功,返回0
  • 若mutex已上锁,pthread_mutex_trylock会加锁失败,返回EBUSY

限时上锁

//成功返回0,失败返回错误编号
int pthread_mutex_timedlock(pthread_mutex_t *mutex, const struct timespec *time);

pthread_mutex_timedlock是一个可以设置阻塞时间的上锁函数:

  • 当mutex已上锁时,调用线程会阻塞设定的时间
  • 当达到设定时间时,pthread_mutex_timedlock将加锁失败并解除阻塞,返回ETIMEDOUT

关于第二个参数time,有两点需要注意:

  • time表示等待的绝对时间,需要将其设为当前时间 + 等待时间
  • time是由struct timespec指定的,它由秒和纳秒来描述时间

示例代码

/*
 * 测试使用上述4个加锁函数
*/

#include <pthread.h>
#include <time.h>
#include <errno.h>
#include <stdio.h>

pthread_mutex_t mutex1;
pthread_mutex_t mutex2;
pthread_mutex_t mutex3;

void *thread1_start(void *arg)
{
    pthread_mutex_lock(&mutex1);
    printf("thread1 has locked mutex1\n");
    sleep(2); //保证thread2执行时mutex1还未解锁
    pthread_mutex_unlock(&mutex1);
}

void *thread2_start(void *arg)
{
    if (pthread_mutex_trylock(&mutex2) == 0)
        printf("thread2 trylock mutex2 sucess\n");

    if (pthread_mutex_trylock(&mutex1) == EBUSY)
        printf("thread2 trylock mutex1 failed\n");
          
    pthread_mutex_unlock(&mutex2);
}

void *thread3_start(void *arg)
{
    struct timespec time;
    struct tm *tmp_time;
    char s[64];
    int err;
    
    pthread_mutex_lock(&mutex3);
    printf("thread3 has locked mutex3\n");
    
    /*获取当前时间,并转化为本地时间打印*/
    clock_gettime(CLOCK_REALTIME, &time);
    tmp_time = localtime(&time.tv_sec);
    strftime(s, sizeof(s), "%r", tmp_time);
    printf("current time is %s\n", s);
    
    /*设置time = 当前时间 + 等待时间10S*/
    time.tv_sec = time.tv_sec + 10;
    
    /*mutex3已上锁,这里会阻塞*/
    if (pthread_mutex_timedlock(&mutex3, &time) == ETIMEDOUT)
        printf("pthread_mutex_timedlock mutex3 timeout\n");
    
    /*再次获取当前时间,并转化为本地时间打印*/
    clock_gettime(CLOCK_REALTIME, &time);
    tmp_time = localtime(&time.tv_sec);
    strftime(s, sizeof(s), "%r", tmp_time);
    printf("the time is now %s\n", s);  
        
    pthread_mutex_unlock(&mutex3);
}

int main()
{     
    pthread_t tid1;
    pthread_t tid2;
    pthread_t tid3;
    
    /*测试pthread_mutex_lock和pthread_mutex_trylock*/
    pthread_mutex_init(&mutex1, NULL);
    pthread_mutex_init(&mutex2, NULL);
       
    pthread_create(&tid1, NULL, thread1_start, NULL);   
    pthread_create(&tid2, NULL, thread2_start, NULL);
    
    if (pthread_join(tid1, NULL) == 0)
    {
        pthread_mutex_destroy(&mutex1);
    }
    
    if (pthread_join(tid2, NULL) == 0)
    {
        pthread_mutex_destroy(&mutex2);
    }    
    
    /*测试pthread_mutex_timedlock*/
    pthread_mutex_init(&mutex3, NULL);
    pthread_create(&tid3, NULL, thread3_start, NULL);
      
    if (pthread_join(tid3, NULL) == 0)
    {
        pthread_mutex_destroy(&mutex3);
    }      
    
    return 0;
}

3. 避免死锁

线程的死锁概念

线程间死锁,指的是线程间相互等待临界资源而造成彼此无法继续执行的现象。

产生死锁的四个必要条件

  • 互斥条件:资源同时只能被一个线程使用,此时若有其他线程请求该资源,则请求线程必须等待
  • 不可剥夺条件:线程获得的资源在未使用完毕前,不能被其他线程抢夺,只能由获得该资源的线程主动释放
  • 请求与保持条件:线程已经至少得到了一个资源,但又提出了新的资源请求,而新的资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放
  • 循环等待条件:存在一个资源等待环,环中每一个线程都占有下一个线程所需的至少一个资源

直观上看,循环等待条件似乎和死锁的定义一样,其实不然,因为死锁定义中的要求更为严格:

  • 循环等待条件要求P(i+1)需要的资源,至少有一个来自P(i)即可
  • 死锁定义要求P(i+1)需要的资源,由且仅由P(i)提供

如何避免死锁

  • 所有线程以相同顺序加锁
  • 给所有的临界资源分配一个唯一的序号,对应的线程锁也分配同样的序号,系统中的所有线程按照严格递增的次序请求资源
  • 使用pthread_mutex_trylock尝试加锁,若失败就放弃上锁,同时释放已占有的锁
  • 使用pthread_mutex_timedlock限时加锁,若超时就放弃上锁,同时释放已占有的锁

4. 条件变量

条件变量概念

  • 条件变量是线程另一种可用的同步机制,它给多线程提供了一个回合的场所
  • 条件变量本身需要由互斥锁保护,线程在改变条件之前必须先上锁,其他线程在获得互斥锁之前不会知道条件发生了改变
  • 条件变量和互斥锁一起使用,可以使线程以无竞争的方式等待特定条件的发生

条件变量基本API

初始化与销毁

条件变量是用pthread_cond_t数据类型表示的,和互斥锁类似,条件变量的初始化方法也有两种:

  • 设置为常量PTHREAD_COND_INITIALIZER,只适用于静态分配的条件变量
  • 调用pthread_cond_init函数,适用于静态分配和动态分配的条件变量

条件变量使用完以后,可以调用pthread_cond_destroy进行销毁,同样的,如果是动态分配的条件变量,在释放内存前,该操作也是必须的。

pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

//两个函数的返回值:成功返回0,失败返回错误编号
int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

其中,pthread_cond_init的第二个参数attr用于设置条件变量的属性,如果要使用默认属性,只需把attr设为NULL。

等待条件满足

//两个函数的返回值:成功返回0,失败返回错误编号
int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);
int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *timeout);

可以调用pthread_cond_wait函数等待条件满足,使用步骤如下,传递给函数的互斥锁对条件进行保护,在条件满足之前,调用线程将一直阻塞。

  • 调用线程将锁住的互斥量传给pthread_cond_wait
  • pthread_cond_wait自动把调用线程放到等待条件的线程列表上,然后对互斥锁解锁
  • 当条件满足,pthread_cond_wait返回时,互斥锁再次被锁住
  • pthread_cond_wait返回后,调用线程再对互斥锁解锁

pthread_cond_timedwait是一个限时等待条件满足的函数,如果发生超时时条件还没满足,pthread_cond_timedwait将重新对互斥锁上锁,然后返回ETIMEDOUT错误。

注意:当条件满足从pthread_cond_wait和pthread_cond_timedwait返回时,调用线程必须重新计算条件,因为另一个线程可能已经在运行并改变了条件。

给线程发信号

有两个函数可以用于通知线程条件已经满足:

  • pthread_cond_signal至少能唤醒一个等待该条件的线程
  • pthread_cond_broadcast可以唤醒等待该条件的所有线程
//两个函数的返回值:成功返回0,失败返回错误编号
int pthread_cond_signal(pthread_cond_t *cond);
int pthread_cond_broadcast(pthread_cond_t *cond);

在调用上面两个函数时,我们说这是在给线程发信号,注意,一定要先获取互斥锁,再改变条件,然后给线程发信号,最后再对互斥锁解锁。

示例代码

/*
 * 结合使用条件变量和互斥锁进行线程同步
*/

#include <pthread.h>
#include <stdio.h>

static pthread_cond_t  cond;
static pthread_mutex_t mutex;
static int             cond_value;
static int             quit;

void *thread_signal(void *arg)
{
    while (!quit)
    {
        pthread_mutex_lock(&mutex);
        cond_value++;                //改变条件,使条件满足
        pthread_cond_signal(&cond);  //给线程发信号 
        printf("signal send, cond_value: %d\n", cond_value);
        pthread_mutex_unlock(&mutex);
        sleep(1);
    }    
}

void *thread_wait(void *arg)
{
    while (!quit)
    {
        pthread_mutex_lock(&mutex);
        
        /*通过while (cond is true)来保证从pthread_cond_wait成功返回时,调用线程会重新检查条件*/
        while (cond_value == 0)
            pthread_cond_wait(&cond, &mutex);
            
        cond_value--;
        printf("signal recv, cond_value: %d\n", cond_value);
        
        pthread_mutex_unlock(&mutex);
        sleep(1);
    } 
}

int main()
{     
    pthread_t tid1;
    pthread_t tid2;
    
    pthread_cond_init(&cond, NULL);
    pthread_mutex_init(&mutex, NULL);
       
    pthread_create(&tid1, NULL, thread_signal, NULL);   
    pthread_create(&tid2, NULL, thread_wait, NULL);
    
    sleep(5);
    quit = 1;
    
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);
    
    pthread_cond_destroy(&cond);
    pthread_mutex_destroy(&mutex);
      
    return 0;
}

转载至:https://zhuanlan.zhihu.com/p/633169684

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1028811.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

阿里云服务器+Frp+Proxifier工具进行内网穿透

阿里云服务器FrpProxifier工具进行内网穿透 为什么进行内网穿透&#xff1f; 什么叫内网穿透&#xff1f; 首先我们对内网和外网这两个名词做个解释&#xff1a; 内网&#xff1a;是内部建立的局域网络或办公网络,例如家庭内部网络&#xff0c;公司内部网络&#xff1b; 外…

如何通过AI视频智能分析技术,构建着装规范检测/工装穿戴检测系统?

众所周知&#xff0c;规范着装在很多场景中起着重要的作用。违规着装极易增加安全隐患&#xff0c;并且引发安全事故和质量问题&#xff0c;例如&#xff0c;在化工工厂中&#xff0c;倘若员工没有穿戴符合要求的特殊防护服和安全鞋&#xff0c;将有极大可能受到有害物质的侵害…

9.18日学习记录

1.VS2019Qt获取电脑设备名称 &#xff08;1&#xff09;添加网络模块network VS2019 Qt 怎么添加Qt模块&#xff1f;_vs 2019 qt widget应用程序泵设置 qt module_令狐掌门的博客-CSDN博客 &#xff08;2&#xff09;关键代码&#xff1a; #include <QHostInfo> QSt…

中小企业生产信息化系统哪个好用?选亿发制造业管理系统提供商

中小型制造企业虽然规模相对较小&#xff0c;但同样是市场经济的重要组成部分。要在这个竞争环境中脱颖而出&#xff0c;智能化生产管理系统成为中小型制造企业不可或缺的工具。让各部门之间的数据无缝衔接&#xff0c;实现工厂的整体协调性和工作效率的大幅提升。 让我们从几个…

【go语言】条件,选择,循环和特殊语句

if语句 a:10 if a>20{fmt.Printf("a大于20") }else if a<10{fmt.Printf("a小于10") }else{fmt.Printf("a大于等于10&#xff0c;a小于等于20") }go语言的if语句和C语言的if语句的差不多&#xff0c;需要注意的是else 和else if要写在括号…

首批Sui教育资助计划获奖者公布,超过30万美元奖金已发放

我们非常高兴地宣布Sui教育资助计划的第一批获奖者&#xff0c;该资助计划旨在支持那些有兴趣培养下一代创新者的开发者。本轮共有9个团队获得了超过30万美元的资助&#xff0c;所选提案包括开发新手营、课程、游戏化学习任务、开发者教育工具和技术workshops等。 Sui教育资助…

手机弱网测试工具:Charles

我们在测试app的时候&#xff0c;需要测试弱网情况下的一些场景&#xff0c;那么使用Charles如何设置弱网呢&#xff0c;请看以下步骤&#xff1a; 前提条件&#xff1a; 手机和电脑要在同一局域网内 Charles连接手机抓包 一、打开Charles&#xff0c;点击代理&#xff0c;…

Java笔记一

D:\java_dev\Java\jdk-20 创建 E:\javawork Hello World! E:\javawork-----新建HelloWorldApp.java 记事本打开文件 一&#xff1a; 在HelloWorldApp.java输入代码 public class HelloWorldApp{public static void main (String args[]){​ System.out.println(“H…

城市级数字孪生底座平台具有新内涵

关键词&#xff1a;数字孪生、数字孪生系统、数字孪生发展、城市数字孪生 **数字孪生城市是新型智慧城市建设新模式。**智慧城市建设模式是不断发展演化的&#xff0c;与上一阶段智慧城市建设相比&#xff0c;**内容形式上&#xff0c;**数字孪生城市建设更加注重城市全实体要…

Rust中的结构体

专栏简介&#xff1a;本专栏作为Rust语言的入门级的文章&#xff0c;目的是为了分享关于Rust语言的编程技巧和知识。对于Rust语言&#xff0c;虽然历史没有C、和python历史悠远&#xff0c;但是它的优点可以说是非常的多&#xff0c;既继承了C运行速度&#xff0c;还拥有了Java…

人工智能的未来:从 Jetson 到 GPT,沙龙见闻与洞察

前言 在当今数字化时代&#xff0c;人工智能正以惊人的速度改变着我们的生活和工作方式。从智能语音助手到自动驾驶汽车&#xff0c;从智能家居到医疗诊断&#xff0c;人工智能技术已经广泛渗透到各个行业&#xff0c;并为其带来了巨大的变革和创新。越来越多的行业专家、学者…

MQTT服务器搭建

本次搭建的MQTT服务器是emqx提供的服务器 1、下载 https://www.emqx.com/en/downloads/broker 从官网下载5.2.0版本emqx-5.2.0-windows-amd64.zip 下载完成直接安装 2、配置&#xff0c;修改端口号 mqtt默认端口号 常规的用法&#xff0c;我们一般使用和开放这两个端口&am…

037:vue项目监听页面变化,动态设置iframe元素高度

第037个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

Element-ui 标签页el-radio-button左边框不显示问题

想实现一个tab切换的功能 看tabs组件挺相似的直接拿来用 默认是下边的效果 分开点 发现右边的白的按钮左边框没了 选中右边 左边按钮的边框正常 emmmm 选择按钮后查看样式 发现他有个border-left:0 去掉之后显示正常 在代码里添加border属性后显示正常了 但又出现新的问题…

二、飞线或者路线的实现(TubeBufferGeometry:管道缓冲几何体)

在做three大屏的时候我们经常会遇到绘制发光路线和指向的情况&#xff0c;那么就需要使用到管道&#xff08;TubeBufferGeometry&#xff09;这个API。先看看他能达到的几种效果。 一、效果图 1平面效果 2飞线效果 二、那么我们直接看代码&#xff0c;一共有6步。 定义数据 l…

作业错题一

1、内联函数的错题 首先&#xff0c;对内联函数的定义不清楚&#xff0c;同时对内联函数的原理有点模糊&#xff0c;还有一些注意的点忽略了&#xff1b; this指针的理解也出现错误&#xff01; 下一题是构造函数和析构函数的错误&#xff1a; 构造函数时先定义先构造&#…

无法删除目录(设备或资源忙)

文章目录 无法删除目录&#xff08;设备或资源忙&#xff09;问题原因解决方案步骤一&#xff1a;首先找到挂载的位置步骤二&#xff1a;取消挂载步骤三&#xff1a;查看挂在情况 无法删除目录&#xff08;设备或资源忙&#xff09; 问题 原因 网络共享挂载导致无法删除 解决…

Java Gradle

目录 1. Gradle简介 1.1 什么是Gradle 1.2 Gradle优点 1.2.1 自动化构建 1.2.2 Gradle相当于构建工具的进化版 1.2.3 对于Gradle优点&#xff0c;官网是这么说的 1.3 Grade 的版本说明 2. Groovy语言基础 2.1 基础语法 2.2 String 与 GString 2.2.1 String 2.2.2 插…

最新SSL证书申请源码,支持API接口,支付在线充值

最新SSL证书申请源码&#xff0c;支持API接口&#xff0c;支付在线充值 目前测试还不需要授权&#xff0c;以后更新版就不知道了 SSL证书保证网络安全的基本保障。向您介绍我们的在线生成SSL证书系统 支持在线生成SSL证书系统&#xff0c;用户登录可在线申请SSL&#xff0c;…

Nacos内核设计之一致性协议(上)

Nacos一致性协议 Nacos技术架构 先简单介绍下Nacos的技术架构 从而对nacos有一个整体的认识 如图Nacos架构分为四层 用户层、应用层、核心层、各种插件 再深入分析下nacos一致性协议的发展过程及原理实现 为什么nacos需要一致性协议 Nacos是一个需要存储数据的一个组件 为了实…