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

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.xdnf.cn/news/140961.html

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

相关文章

【性能测试】JMeter:集合点,同步定时器的应用实例!

一、集合点的定义 在性能测试过程中&#xff0c;为了真实模拟多个用户同时进行操作以度量服务器的处理能力&#xff0c;可以考虑同步虚拟用户以便恰好在同一时刻执行操作或发送请求。 通过插入集合点可以较真实模拟多个用户并发操作。 (注意&#xff1a;虽然通过加入集合点可…

安全防御第二次作业

1. 防火墙支持那些NAT技术&#xff0c;主要应用场景是什么&#xff1f; 防火墙支持几乎所有的NAT技术&#xff0c;包括源NAT、目标NAT、双向NAT等&#xff0c;主要应用场景是保护内部网络免受外部网络的攻击 NAT技术可以将IP数据报文头中的IP地址转换为另一个IP地址&#xff…

JMeter:接口测试基础介绍

一、什么是接口 接口是非常抽象的概念&#xff0c;先来看下中国最大的综合性辞典《辞海》是怎样定义接口的&#xff1a; 两个不同系统或系统中两个不同特性部分的交接部分。一般分硬件接口和软件接口两种。前者是为连接计算机各部分之间、计算机与计算机之间、计算机与外部系统…

Linux离线安装elasticsearch|header|kibna插件最详细

1.准备软件安装包 [hadoophost152 elasticsearch]$ ll -rw-r--r--. 1 hadoop hadoop 515807354 9月 23 23:40 elasticsearch-8.1.1-linux-x86_64.tar.gz -rw-r--r--. 1 hadoop hadoop 1295593 9月 23 23:48 elasticsearch-head-master.tar.gz -rw-r--r--. 1 hadoop hadoop…

SSRF漏洞

Server-Side Request Forgery:服务器端请求伪造 目标&#xff1a;网站的内部系统 形成的原因 攻击者构造形成由服务器端发起请求的译者安全漏洞。 由于服务端提供了从其他服务器应用获取数据的功能&#xff0c;且没有对目标地址做过滤与限制。比如从指定URL地址获取网页文本内…

Mybatis-分页插件

Mybatis-分页插件 前言一、分页插件的使用步骤1.添加依赖2.配置分页插件3. 分页插件的使用 前言 可以通过分页插件在实现以下前端样式时更加方便&#xff1a; 首页 上一页 2 3 4 5 6 下一页 末页 limit index,pageSize pageSize&#xff1a;每页显示的条数 pageNum&#xff…

RASP初识

需要了解的东西. 是什么 拦截日志&#xff1a;rasp/logs/alarm/alarm.log RASP&#xff08;Runtime application self-protection&#xff09;运行时应用自我保护。 官方英译应用程序不应将大部分运行时保护委托给外部设备。应用程序应该能够自我保护&#xff08;即&#xf…

如何进行销售漏斗管理?

本文将为大家讲解&#xff1a;如何进行销售漏斗管理&#xff1f; 销售漏斗管理是现代销售管理的核心概念之一。它将销售过程分解为一系列阶段&#xff0c;从而帮助销售团队更有效地跟踪和管理潜在客户。本文将深入探讨销售漏斗管理的方法&#xff0c;并结合简道云CRM的实际应用…

LeetCode 494.目标和 (动态规划 + 性能优化)二维数组 压缩成 一维数组

494. 目标和 - 力扣&#xff08;LeetCode&#xff09; 给你一个非负整数数组 nums 和一个整数 target 。 向数组中的每个整数前添加 或 - &#xff0c;然后串联起所有整数&#xff0c;可以构造一个 表达式 &#xff1a; 例如&#xff0c;nums [2, 1] &#xff0c;可以在 2…

Java中如何将String类型的2023年09月21日这个值变成DATE相关的类型

Java中如何将String类型的2023年09月21日这个值变成DATE 可以通过使用Java中的SimpleDateFormat类完成。以下是一个例子&#xff1a; import java.text.SimpleDateFormat; import java.text.ParseException; import java.util.Date;public class Main {public static void ma…

Linux动态库

定义&#xff1a;动态函数库&#xff0c;是在程序执行时动态&#xff08;临时&#xff09;由目标程序去调用 优点&#xff1a; 调用时不复制&#xff0c;程序运行时动态加载到内存&#xff0c;供程序调用&#xff0c;系统只加载一次&#xff0c;多个程序可以共用&#xff0c;…

大厂面试之算法篇

目录 前言 算法对于前端来说重要吗&#xff1f; 期待你的答案 算法 如何学习算法 算法基础知识 时间复杂度 空间复杂度 前端 数据结构 数组 最长递增子序列 买卖股票问题 买卖股票之交易明细 硬币找零问题 数组拼接最小值 奇偶排序 两数之和 三数之和 四数之…

9.19号作业

2> 完成文本编辑器的保存工作 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include <QFontDialog> #include <QFont> #include <QMessageBox> #include <QDebug> #include <QColorDialog> #include <QColor&g…

Python 打印素数

"""打印素数介绍&#xff1a;素数是指只有两个正因数&#xff08;1和它本身&#xff09;的自然数&#xff0c;而且必须大于1。例如&#xff1a;2、3、5、7、11、13、17、19、23、29等等都是素数。小于2的数不是素数&#xff0c;因为它没有两个正因数。例如&…

对话ChatGPT:AIGC时代下,分布式存储的应用与前景

随着科技的飞速发展&#xff0c;我们正步入一个被称为AIGC时代的全新阶段&#xff0c;人工智能、物联网、大数据、云计算成为这个信息爆炸时代的主要特征。自2022年11月以来&#xff0c;ChatGPT的知名度迅速攀升&#xff0c;引发了全球科技爱好者的极大关注&#xff0c;其高超的…

java框架-Springboot3-web开发

文章目录 自动配置默认效果WebMvcAutoConfigurationWebMvcConfigurer接口静态资源访问首页Favicon缓存 自定义静态资源路径1、配置方式2、代码方式 路径匹配规则内容协商默认支持json配置支持xml内容协商原理自定义支持ymal 模板引擎模板引擎Thymeleaf整合基础语法遍历判断属性…

静态资源的动态引入

有常用的2种方式&#xff1a; 1、css中的静态路径 2、img中的src静态路径 运行的环境是打包后的图片路径&#xff0c;而打包后的图片通常会生成一个文件指纹&#xff0c;而我们在写代码时&#xff0c;写的是源码中的路径和文件名&#xff0c;如果是静态路径&#xff0c;则会自动…

升级iOS17后可以降级吗?iOS17退回iOS16方法教程分享

iOS 17已上线几天&#xff0c;从网上用户的反馈和媒体机构的报告来看&#xff0c;iOS17系统对旧机型来说并不友好&#xff0c;除了电池续航下降以外&#xff0c;占用大量储存空间&#xff0c;BUG也不少。 苹果于 9 月 7 日发布了 iOS 16.6.1 版本&#xff0c;如果升级iOS17后发…

What is the difference between Parseval‘s theorem and Plancherel Theorem

Plancherel定理是调和分析里的一个结论, 最早由Michel Plancherel证明, 其可表述为 对同时属于 L 1 ( R ) L^{1}(R) L1(R) 和 L 2 ( R ) L^{2}(R) L2(R) 的函数f来说,其傅立叶变换F属于 L 2 ( R ) L^{2}(R) L2(R) ,且傅立叶变换是等距变换.数学表述为&#xff1a; ∥ f ^ ∥ 2…

正确生成hashCode和equals方法,以及联合Map,set集合达到去重目的

Idea自动生成HashCode和equals视频链接 https://live.csdn.net/v/330419实体类 对name和age两个属性重写hashCode&#xff0c;equals方法 package TestEqualsHashCode; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok…