爆款云主机2核4G限时秒杀,88元/年起!
查看详情

活动

天翼云最新优惠活动,涵盖免费试用,产品折扣等,助您降本增效!
热门活动
  • 618智算钜惠季 爆款云主机2核4G限时秒杀,88元/年起!
  • 免费体验DeepSeek,上天翼云息壤 NEW 新老用户均可免费体验2500万Tokens,限时两周
  • 云上钜惠 HOT 爆款云主机全场特惠,更有万元锦鲤券等你来领!
  • 算力套餐 HOT 让算力触手可及
  • 天翼云脑AOne NEW 连接、保护、办公,All-in-One!
  • 中小企业应用上云专场 产品组合下单即享折上9折起,助力企业快速上云
  • 息壤高校钜惠活动 NEW 天翼云息壤杯高校AI大赛,数款产品享受线上订购超值特惠
  • 天翼云电脑专场 HOT 移动办公新选择,爆款4核8G畅享1年3.5折起,快来抢购!
  • 天翼云奖励推广计划 加入成为云推官,推荐新用户注册下单得现金奖励
免费活动
  • 免费试用中心 HOT 多款云产品免费试用,快来开启云上之旅
  • 天翼云用户体验官 NEW 您的洞察,重塑科技边界

智算服务

打造统一的产品能力,实现算网调度、训练推理、技术架构、资源管理一体化智算服务
智算云(DeepSeek专区)
科研助手
  • 算力商城
  • 应用商城
  • 开发机
  • 并行计算
算力互联调度平台
  • 应用市场
  • 算力市场
  • 算力调度推荐
一站式智算服务平台
  • 模型广场
  • 体验中心
  • 服务接入
智算一体机
  • 智算一体机
大模型
  • DeepSeek-R1-昇腾版(671B)
  • DeepSeek-R1-英伟达版(671B)
  • DeepSeek-V3-昇腾版(671B)
  • DeepSeek-R1-Distill-Llama-70B
  • DeepSeek-R1-Distill-Qwen-32B
  • Qwen2-72B-Instruct
  • StableDiffusion-V2.1
  • TeleChat-12B

应用商城

天翼云精选行业优秀合作伙伴及千余款商品,提供一站式云上应用服务
进入甄选商城进入云市场创新解决方案
办公协同
  • WPS云文档
  • 安全邮箱
  • EMM手机管家
  • 智能商业平台
财务管理
  • 工资条
  • 税务风控云
企业应用
  • 翼信息化运维服务
  • 翼视频云归档解决方案
工业能源
  • 智慧工厂_生产流程管理解决方案
  • 智慧工地
建站工具
  • SSL证书
  • 新域名服务
网络工具
  • 翼云加速
灾备迁移
  • 云管家2.0
  • 翼备份
资源管理
  • 全栈混合云敏捷版(软件)
  • 全栈混合云敏捷版(一体机)
行业应用
  • 翼电子教室
  • 翼智慧显示一体化解决方案

合作伙伴

天翼云携手合作伙伴,共创云上生态,合作共赢
天翼云生态合作中心
  • 天翼云生态合作中心
天翼云渠道合作伙伴
  • 天翼云代理渠道合作伙伴
天翼云服务合作伙伴
  • 天翼云集成商交付能力认证
天翼云应用合作伙伴
  • 天翼云云市场合作伙伴
  • 天翼云甄选商城合作伙伴
天翼云技术合作伙伴
  • 天翼云OpenAPI中心
  • 天翼云EasyCoding平台
天翼云培训认证
  • 天翼云学堂
  • 天翼云市场商学院
天翼云合作计划
  • 云汇计划
天翼云东升计划
  • 适配中心
  • 东升计划
  • 适配互认证

开发者

开发者相关功能入口汇聚
技术社区
  • 专栏文章
  • 互动问答
  • 技术视频
资源与工具
  • OpenAPI中心
开放能力
  • EasyCoding敏捷开发平台
培训与认证
  • 天翼云学堂
  • 天翼云认证
魔乐社区
  • 魔乐社区

支持与服务

为您提供全方位支持与服务,全流程技术保障,助您轻松上云,安全无忧
文档与工具
  • 文档中心
  • 新手上云
  • 自助服务
  • OpenAPI中心
定价
  • 价格计算器
  • 定价策略
基础服务
  • 售前咨询
  • 在线支持
  • 在线支持
  • 工单服务
  • 建议与反馈
  • 用户体验官
  • 服务保障
  • 客户公告
  • 会员中心
增值服务
  • 红心服务
  • 首保服务
  • 客户支持计划
  • 专家技术服务
  • 备案管家

了解天翼云

天翼云秉承央企使命,致力于成为数字经济主力军,投身科技强国伟大事业,为用户提供安全、普惠云服务
品牌介绍
  • 关于天翼云
  • 智算云
  • 天翼云4.0
  • 新闻资讯
  • 天翼云APP
基础设施
  • 全球基础设施
  • 信任中心
最佳实践
  • 精选案例
  • 超级探访
  • 云杂志
  • 分析师和白皮书
  • 天翼云·创新直播间
市场活动
  • 2025智能云生态大会
  • 2024智算云生态大会
  • 2023云生态大会
  • 2022云生态大会
  • 天翼云中国行
天翼云
  • 活动
  • 智算服务
  • 产品
  • 解决方案
  • 应用商城
  • 合作伙伴
  • 开发者
  • 支持与服务
  • 了解天翼云
      • 文档
      • 控制中心
      • 备案
      • 管理中心

      Linux线程安全

      首页 知识中心 软件开发 文章详情页

      Linux线程安全

      2023-05-23 09:46:42 阅读次数:130

      linux,线程

       

      Linux线程互斥

      进程线程间的互斥相关背景概念

      • 临界资源: 多线程执行流共享的资源叫做临界资源。
      • 临界区: 每个线程内部,访问临界资源的代码,就叫做临界区。
      • 互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。
      • 原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成。

      临界资源和临界区

      进程之间如果要进行通信我们需要先创建第三方资源,让不同的进程看到同一份资源,由于这份第三方资源可以由操作系统中的不同模块提供,于是进程间通信的方式有很多种。进程间通信中的第三方资源就叫做临界资源,访问第三方资源的代码就叫做临界区。

      而多线程的大部分资源都是共享的,线程之间进行通信不需要费那么大的劲去创建第三方资源。

      例如,我们只需要在全局区定义一个count变量,让新线程每隔一秒对该变量加一操作,让主线程每隔一秒获取count变量的值进行打印。

      #include <stdio.h>
      #include <pthread.h>
      #include <unistd.h>
      
      int count = 0;
      void* Routine(void* arg)
      {
      	while (1){
      		count++;
      		sleep(1);
      	}
      	pthread_exit((void*)0);
      }
      int main()
      {
      	pthread_t tid;
      	pthread_create(&tid, NULL, Routine, NULL);
      	while (1){
      		printf("count: %d\n", count);
      		sleep(1);
      	}
      	pthread_join(tid, NULL);
      	return 0;
      }
      

      运行结果如下:
      Linux线程安全
      此时我们相当于实现了主线程和新线程之间的通信,其中全局变量count就叫做临界资源,因为它被多个执行流共享,而主线程中的printf和新线程中count++就叫做临界区,因为这些代码对临界资源进行了访问。

      互斥和原子性

      在多线程情况下,如果这多个执行流都自顾自的对临界资源进行操作,那么此时就可能导致数据不一致的问题。解决该问题的方案就叫做互斥,互斥的作用就是,保证在任何时候有且只有一个执行流进入临界区对临界资源进行访问。

      原子性指的是不可被分割的操作,该操作不会被任何调度机制打断,该操作只有两态,要么完成,要么未完成。

      例如,下面我们模拟实现一个抢票系统,我们将记录票的剩余张数的变量定义为全局变量,主线程创建四个新线程,让这四个新线程进行抢票,当票被抢完后这四个线程自动退出。

      #include <stdio.h>
      #include <unistd.h>
      #include <pthread.h>
      
      int tickets = 1000;
      void* TicketGrabbing(void* arg)
      {
      	const char* name = (char*)arg;
      	while (1){
      		if (tickets > 0){
      			usleep(10000);
      			printf("[%s] get a ticket, left: %d\n", name, --tickets);
      		}
      		else{
      			break;
      		}
      	}
      	printf("%s quit!\n", name);
      	pthread_exit((void*)0);
      }
      int main()
      {
      	pthread_t t1, t2, t3, t4;
      	pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
      	pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
      	pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
      	pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
      	
      	pthread_join(t1, NULL);
      	pthread_join(t2, NULL);
      	pthread_join(t3, NULL);
      	pthread_join(t4, NULL);
      	return 0;
      }
      

      运行结果显然不符合我们的预期,因为其中出现了剩余票数为负数的情况。
      Linux线程安全
      该代码中记录剩余票数的变量tickets就是临界资源,因为它被多个执行流同时访问,而判断tickets是否大于0、打印剩余票数以及--tickets这些代码就是临界区,因为这些代码对临界资源进行了访问。

      剩余票数出现负数的原因:

      • if语句判断条件为真以后,代码可以并发的切换到其他线程。
      • usleep用于模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段。
      • --ticket操作本身就不是一个原子操作。

      为什么--ticket不是原子操作?

      我们对一个变量进行--,我们实际需要进行以下三个步骤:

      1. load:将共享变量tickets从内存加载到寄存器中。
      2. update:更新寄存器里面的值,执行-1操作。
      3. store:将新值从寄存器写回共享变量tickets的内存地址。

      Linux线程安全
      --操作对应的汇编代码如下:
      Linux线程安全
      既然--操作需要三个步骤才能完成,那么就有可能当thread1刚把tickets的值读进CPU就被切走了,也就是从CPU上剥离下来,假设此时thread1读取到的值就是1000,而当thread1被切走时,寄存器中的1000叫做thread1的上下文信息,因此需要被保存起来,之后thread1就被挂起了。
      Linux线程安全
      假设此时thread2被调度了,由于thread1只进行了--操作的第一步,因此thread2此时看到tickets的值还是1000,而系统给thread2的时间片可能较多,导致thread2一次性执行了100次--才被切走,最终tickets由1000减到了900。
      Linux线程安全
      此时系统再把thread1恢复上来,恢复的本质就是继续执行thread1的代码,并且要将thread1曾经的硬件上下文信息恢复出来,此时寄存器当中的值是恢复出来的1000,然后thread1继续执行--操作的第二步和第三步,最终将999写回内存。
      Linux线程安全
      在上述过程中,thread1抢了1张票,thread2抢了100张票,而此时剩余的票数却是999,也就相当于多出了100张票。

      因此对一个变量进行--操作并不是原子的,虽然--tickets看起来就是一行代码,但这行代码被编译器编译后本质上是三行汇编,相反,对一个变量进行++也需要对应的三个步骤,即++操作也不是原子操作。

      互斥量mutex

      • 大部分情况,线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况变量归属单个线程,其他线程无法获得这种变量。
      • 但有时候,很多变量都需要在线程间共享,这样的变量成为共享变量,可以通过数据的共享,完成线程之间的交互。
      • 多个线程并发的操作共享变量,就会带来一些问题。

      要解决上述抢票系统的问题,需要做到三点:

      • 代码必须有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
      • 如果多个线程同时要求执行临界区的代码,并且此时临界区没有线程在执行,那么只能允许一个线程进入该临界区。
      • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

      要做到这三点,本质上就是需要一把锁,Linux上提供的这把锁叫互斥量。
      Linux线程安全

      互斥量的接口

      初始化互斥量

      初始化互斥量的函数叫做pthread_mutex_init,该函数的函数原型如下:

      int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr);
      

      参数说明:

      • mutex:需要初始化的互斥量。
      • attr:初始化互斥量的属性,一般设置为NULL即可。

      返回值说明:

      • 互斥量初始化成功返回0,失败返回错误码。

      调用pthread_mutex_init函数初始化互斥量叫做动态分配,除此之外,我们还可以用下面这种方式初始化互斥量,该方式叫做静态分配:

      pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
      

      销毁互斥量

      销毁互斥量的函数叫做pthread_mutex_destroy,该函数的函数原型如下:

      int pthread_mutex_destroy(pthread_mutex_t *mutex);
      

      参数说明:

      • mutex:需要销毁的互斥量。

      返回值说明:

      • 互斥量销毁成功返回0,失败返回错误码。

      销毁互斥量需要注意:

      • 使用PTHREAD_MUTEX_INITIALIZER初始化的互斥量不需要销毁。
      • 不要销毁一个已经加锁的互斥量。
      • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁。

      互斥量加锁

      互斥量加锁的函数叫做pthread_mutex_lock,该函数的函数原型如下:

      int pthread_mutex_lock(pthread_mutex_t *mutex);
      

      参数说明:

      • mutex:需要加锁的互斥量。

      返回值说明:

      • 互斥量加锁成功返回0,失败返回错误码。

      调用pthread_mutex_lock时,可能会遇到以下情况:

      1. 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功。
      2. 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_mutex_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

      互斥量解锁

      互斥量解锁的函数叫做pthread_mutex_unlock,该函数的函数原型如下:

      int pthread_mutex_unlock(pthread_mutex_t *mutex);
      

      参数说明:

      • mutex:需要解锁的互斥量。

      返回值说明:

      • 互斥量解锁成功返回0,失败返回错误码。

      使用示例:

      例如,我们在上述的抢票系统中引入互斥量,每一个线程要进入临界区之前都必须先申请锁,只有申请到锁的线程才可以进入临界区对临界资源进行访问,并且当线程出临界区的时候需要释放锁,这样才能让其余要进入临界区的线程继续竞争锁。

      #include <stdio.h>
      #include <unistd.h>
      #include <pthread.h>
      
      int tickets = 1000;
      pthread_mutex_t mutex;
      void* TicketGrabbing(void* arg)
      {
      	const char* name = (char*)arg;
      	while (1){
      		pthread_mutex_lock(&mutex);
      		if (tickets > 0){
      			usleep(100);
      			printf("[%s] get a ticket, left: %d\n", name, --tickets);
      			pthread_mutex_unlock(&mutex);
      		}
      		else{
      			pthread_mutex_unlock(&mutex);
      			break;
      		}
      	}
      	printf("%s quit!\n", name);
      	pthread_exit((void*)0);
      }
      int main()
      {
      	pthread_mutex_init(&mutex, NULL);
      	pthread_t t1, t2, t3, t4;
      	pthread_create(&t1, NULL, TicketGrabbing, "thread 1");
      	pthread_create(&t2, NULL, TicketGrabbing, "thread 2");
      	pthread_create(&t3, NULL, TicketGrabbing, "thread 3");
      	pthread_create(&t4, NULL, TicketGrabbing, "thread 4");
      
      	pthread_join(t1, NULL);
      	pthread_join(t2, NULL);
      	pthread_join(t3, NULL);
      	pthread_join(t4, NULL);
      	pthread_mutex_destroy(&mutex);
      	return 0;
      }
      

      运行代码,此时在抢票过程中就不会出现票数剩余为负数的情况了。
      Linux线程安全
      注意:

      • 在大部分情况下,加锁本身都是有损于性能的事,它让多执行流由并行执行变为了串行执行,这几乎是不可避免的。
      • 我们应该在合适的位置进行加锁和解锁,这样能尽可能减少加锁带来的性能开销成本。
      • 进行临界资源的保护,是所有执行流都应该遵守的标准,这时程序员在编码时需要注意的。

      互斥量实现原理探究

      加锁后的原子性体现在哪里?

      引入互斥量后,当一个线程申请到锁进入临界区时,在其他线程看来该线程只有两种状态,要么没有申请锁,要么锁已经释放了,因为只有这两种状态对其他线程才是有意义的。

      例如,图中线程1进入临界区后,在线程2、3、4看来,线程1要么没有申请锁,要么线程1已经将锁释放了,因为只有这两种状态对线程2、3、4才是有意义的,当线程2、3、4检测到其他状态时也就被阻塞了。
      Linux线程安全
      此时对于线程2、3、4而言,它们就认为线程1的整个操作过程是原子的。

      临界区内的线程可能进行线程切换吗?

      临界区内的线程完全可能进行线程切换,但即便该线程被切走,其他线程也无法进入临界区进行资源访问,因为此时该线程是拿着锁被切走的,锁没有被释放也就意味着其他线程无法申请到锁,也就无法进入临界区进行资源访问了。

      其他想进入该临界区进行资源访问的线程,必须等该线程执行完临界区的代码并释放锁之后,才能申请锁,申请到锁之后才能进入临界区。

      锁是否需要被保护?

      我们说被多个执行流共享的资源叫做临界资源,访问临界资源的代码叫做临界区。所有的线程在进入临界区之前都必须竞争式的申请锁,因此锁也是被多个执行流共享的资源,也就是说锁本身就是临界资源。

      既然锁是临界资源,那么锁就必须被保护起来,但锁本身就是用来保护临界资源的,那锁又由谁来保护的呢?

      锁实际上是自己保护自己的,我们只需要保证申请锁的过程是原子的,那么锁就是安全的。

      如何保证申请锁的过程是原子的?

      • 上面我们已经说明了--和++操作不是原子操作,可能会导致数据不一致问题。
      • 为了实现互斥锁操作,大多数体系结构都提供了swap或exchange指令,该指令的作用就是把寄存器和内存单元的数据相交换。
      • 由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的总线周期也有先后,一个处理器上的交换指令执行时,另一个处理器的交换指令只能等待总线周期。

      操作系统的工作原理:

      • 操作系统一旦启动成功后就是一个死循环。
      • 时钟是计算机中的一个硬件,时钟每隔一段时间会向操作系统发起一个时钟中断,操作系统就会根据时钟中断去执行中断向量表。
      • 中断向量表本质上就是一个函数表,比如刷磁盘的函数、检测网卡的函数以及刷新数据的函数等等。
      • 计算机不断向操作系统发起时钟中断,操作系统就根据时钟中断,不断地去执行对应的代码。
      • CPU有多个,但总线只有一套。CPU和内存都是计算机中的硬件,这两个硬件之间要进行数据交互一定是用线连接起来的,其中我们把CPU和内存连接的线叫做系统总线,把内存和外设连接起来的线叫做IO总线。
      • 系统总线只有一套,有的时候CPU访问内存是想从内存中读取指令,有的时候是想从内存读取数据,所以总线是被不同的操作种类共享的。计算机是通过总线周期来区分此时总线当中传输的是哪种资源的。

      下面我们来看看lock和unlock的伪代码:
      Linux线程安全
      我们可以认为mutex的初始值为1,al是计算机中的一个寄存器,当线程申请锁时,需要执行以下步骤:

      1. 先将al寄存器中的值清0。该动作可以被多个线程同时执行,因为每个线程都有自己的一组寄存器(上下文信息),执行该动作本质上是将自己的al寄存器清0。
      2. 然后交换al寄存器和mutex中的值。xchgb是体系结构提供的交换指令,该指令可以完成寄存器和内存单元之间数据的交换。
      3. 最后判断al寄存器中的值是否大于0。若大于0则申请锁成功,此时就可以进入临界区访问对应的临界资源;否则申请锁失败需要被挂起等待,直到锁被释放后再次竞争申请锁。

      例如,此时内存中mutex的值为1,线程申请锁时先将al寄存器中的值清0,然后将al寄存器中的值与内存中mutex的值进行交换。
      Linux线程安全
      交换完成后检测该线程的al寄存器中的值为1,则该线程申请锁成功,可以进入临界区对临界资源进行访问。

      而此后的线程若是再申请锁,与内存中的mutex交换得到的值就是0了,此时该线程申请锁失败,需要被挂起等待,直到锁被释放后再次竞争申请锁。
      Linux线程安全
      当线程释放锁时,需要执行以下步骤:

      1. 将内存中的mutex置回1。使得下一个申请锁的线程在执行交换指令后能够得到1,形象地说就是“将锁的钥匙放回去”。
      2. 唤醒等待Mutex的线程。唤醒这些因为申请锁失败而被挂起的线程,让它们继续竞争申请锁。

      注意:

      • 在申请锁时本质上就是哪一个线程先执行了交换指令,那么该线程就申请锁成功,因为此时该线程的al寄存器中的值就是1了。而交换指令就只是一条汇编指令,一个线程要么执行了交换指令,要么没有执行交换指令,所以申请锁的过程是原子的。
      • 在线程释放锁时没有将当前线程al寄存器中的值清0,这不会造成影响,因为每次线程在申请锁时都会先将自己al寄存器中的值清0,再执行交换指令。
      • CPU内的寄存器不是被所有的线程共享的,每个线程都有自己的一组寄存器,但内存中的数据是各个线程共享的。申请锁实际就是,把内存中的mutex通过交换指令,原子性的交换到自己的al寄存器中。

      可重入VS线程安全

      概念

      • 线程安全: 多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现线程安全问题。
      • 重入: 同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则是不可重入函数。

      注意: 线程安全讨论的是线程执行代码时是否安全,重入讨论的是函数被重入进入。

      常见的线程不安全的情况

      • 不保护共享变量的函数。

      • 函数状态随着被调用,状态发生变化的函数。

      • 返回指向静态变量指针的函数。

      • 调用线程不安全函数的函数。

      常见的线程安全的情况

      • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的。
      • 类或者接口对于线程来说都是原子操作。
      • 多个线程之间的切换不会导致该接口的执行结果存在二义性。

      常见的不可重入的情况

      • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的。
      • 调用了标准I/O库函数,标准I/O可以的很多实现都是以不可重入的方式使用全局数据结构。
      • 可重入函数体内使用了静态的数据结构。

      常见的可重入的情况

      • 不使用全局变量或静态变量。
      • 不使用malloc或者new开辟出的空间。
      • 不调用不可重入函数。
      • 不返回静态或全局数据,所有数据都由函数的调用者提供。
      • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据。

      可重入与线程安全联系

      • 函数是可重入的,那就是线程安全的。
      • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题。
      • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。

      可重入与线程安全区别

      • 可重入函数是线程安全函数的一种。
      • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
      • 如果对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数的锁还未释放则会产生死锁,因此是不可重入的。

      常见锁概念

      死锁

      • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所占用不会释放的资源而处于的一种永久等待状态。

      单执行流可能产生死锁吗?

      单执行流也有可能产生死锁,如果某一执行流连续申请了两次锁,那么此时该执行流就会被挂起。因为该执行流第一次申请锁的时候是申请成功的,但第二次申请锁时因为该锁已经被申请过了,于是申请失败导致被挂起直到该锁被释放时才会被唤醒,但是这个锁本来就在自己手上,自己现在处于被挂起的状态根本没有机会释放锁,所以该执行流将永远不会被唤醒,此时该执行流也就处于一种死锁的状态。

      例如,在下面的代码中我们让主线程创建的新线程连续申请了两次锁。

      #include <stdio.h>
      #include <pthread.h>
      
      pthread_mutex_t mutex;
      void* Routine(void* arg)
      {
      	pthread_mutex_lock(&mutex);
      	pthread_mutex_lock(&mutex);
      	
      	pthread_exit((void*)0);
      }
      int main()
      {
      	pthread_t tid;
      	pthread_mutex_init(&mutex, NULL);
      	pthread_create(&tid, NULL, Routine, NULL);
      	
      	pthread_join(tid, NULL);
      	pthread_mutex_destroy(&mutex);
      	return 0;
      }
      

      运行代码,此时该程序实际就处于一种被挂起的状态。
      Linux线程安全
      用ps命令查看该进程时可以看到,该进程当前的状态是Sl+,其中的l实际上就是lock的意思,表示该进程当前处于一种死锁的状态。
      Linux线程安全

      什么叫做阻塞?

      进程运行时是被CPU调度的,换句话说进程在调度时是需要用到CPU资源的,每个CPU都有一个运行等待队列(runqueue),CPU在运行时就是从该队列中获取进程进行调度的。
      Linux线程安全
      在运行等待队列中的进程本质上就是在等待CPU资源,实际上不止是等待CPU资源如此,等待其他资源也是如此,比如锁的资源、磁盘的资源、网卡的资源等等,它们都有各自对应的资源等待队列。
      Linux线程安全

      例如,当某一个进程在被CPU调度时,该进程需要用到锁的资源,但是此时锁的资源正在被其他进程使用:

      • 那么此时该进程的状态就会由R状态变为某种阻塞状态,比如S状态。并且该进程会被移出运行等待队列,被链接到等待锁的资源的资源等待队列,而CPU则继续调度运行等待队列中的下一个进程。
      • 此后若还有进程需要用到这一个锁的资源,那么这些进程也都会被移出运行等待队列,依次链接到这个锁的资源等待队列当中。
      • 直到使用锁的进程已经使用完毕,也就是锁的资源已经就绪,此时就会从锁的资源等待队列中唤醒一个进程,将该进程的状态由S状态改为R状态,并将其重新链接到运行等待队列,等到CPU再次调度该进程时,该进程就可以使用到锁的资源了。

      总结一下:

      • 站在操作系统的角度,进程等待某种资源,就是将当前进程的task_struct放入对应的等待队列,这种情况可以称之为当前进程被挂起等待了。
      • 站在用户角度,当进程等待某种资源时,用户看到的就是自己的进程卡住不动了,我们一般称之为应用阻塞了。
      • 这里所说的资源可以是硬件资源也可以是软件资源,锁本质就是一种软件资源,当我们申请锁时,锁当前可能并没有就绪,可能正在被其他线程所占用,此时当其他线程再来申请锁时,就会被放到这个锁的资源等待队列当中。

      死锁的四个必要条件

      • 互斥条件: 一个资源每次只能被一个执行流使用。
      • 请求与保持条件: 一个执行流因请求资源而阻塞时,对已获得的资源保持不放。
      • 不剥夺条件: 一个执行流已获得的资源,在未使用完之前,不能强行剥夺。
      • 循环等待条件: 若干执行流之间形成一种头尾相接的循环等待资源的关系。

      注意: 这是死锁的四个必要条件,也就是说只有同时满足了这四个条件才可能产生死锁。

      避免死锁

      • 破坏死锁的四个必要条件。
      • 加锁顺序一致。
      • 避免锁未释放的场景。
      • 资源一次性分配。

      除此之外,还有一些避免死锁的算法,比如死锁检测算法和银行家算法。

      Linux线程同步

      同步概念与竞态条件

      同步: 在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,这就叫做同步。
      竞态条件: 因为时序问题,而导致程序异常,我们称之为竞态条件。

      • 首先需要明确的是,单纯的加锁是会存在某些问题的,如果个别线程的竞争力特别强,每次都能够申请到锁,但申请到锁之后什么也不做,所以在我们看来这个线程就一直在申请锁和释放锁,这就可能导致其他线程长时间竞争不到锁,引起饥饿问题。
      • 单纯的加锁是没有错的,它能够保证在同一时间只有一个线程进入临界区,但它没有高效的让每一个线程使用这份临界资源。
      • 现在我们增加一个规则,当一个线程释放锁后,这个线程不能立马再次申请锁,该线程必须排到这个锁的资源等待队列的最后。
      • 增加这个规则之后,下一个获取到锁的资源的线程就一定是在资源等待队列首部的线程,如果有十个线程,此时我们就能够让这十个线程按照某种次序进行临界资源的访问。

      例如,现在有两个线程访问一块临界区,一个线程往临界区写入数据,另一个线程从临界区读取数据,但负责数据写入的线程的竞争力特别强,该线程每次都能竞争到锁,那么此时该线程就一直在执行写入操作,直到临界区被写满,此后该线程就一直在进行申请锁和释放锁。而负责数据读取的线程由于竞争力太弱,每次都申请不到锁,因此无法进行数据的读取,引入同步后该问题就能很好的解决。

      条件变量

      条件变量是利用线程间共享的全局变量进行同步的一种机制,条件变量是用来描述某种资源是否就绪的一种数据化描述。

      条件变量主要包括两个动作:

      • 一个线程等待条件变量的条件成立而被挂起。
      • 另一个线程使条件成立后唤醒等待的线程。

      条件变量通常需要配合互斥锁一起使用。

      条件变量函数

      初始化条件变量

      初始化条件变量的函数叫做pthread_cond_init,该函数的函数原型如下:

      int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
      

      参数说明:

      • cond:需要初始化的条件变量。
      • attr:初始化条件变量的属性,一般设置为NULL即可。

      返回值说明:

      • 条件变量初始化成功返回0,失败返回错误码。

      调用pthread_cond_init函数初始化条件变量叫做动态分配,除此之外,我们还可以用下面这种方式初始化条件变量,该方式叫做静态分配:

      pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
      

      销毁条件变量

      销毁条件变量的函数叫做pthread_cond_destroy,该函数的函数原型如下:

      int pthread_cond_destroy(pthread_cond_t *cond);
      

      参数说明:

      • cond:需要销毁的条件变量。

      返回值说明:

      • 条件变量销毁成功返回0,失败返回错误码。

      销毁条件变量需要注意:

      • 使用PTHREAD_COND_INITIALIZER初始化的条件变量不需要销毁。

      等待条件变量满足

      等待条件变量满足的函数叫做pthread_cond_wait,该函数的函数原型如下:

      int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
      

      参数说明:

      • cond:需要等待的条件变量。
      • mutex:当前线程所处临界区对应的互斥锁。

      返回值说明:

      • 函数调用成功返回0,失败返回错误码。

      唤醒等待

      唤醒等待的函数有以下两个:

      int pthread_cond_broadcast(pthread_cond_t *cond);
      int pthread_cond_signal(pthread_cond_t *cond);
      

      区别:

      • pthread_cond_signal函数用于唤醒等待队列中首个线程。
      • pthread_cond_broadcast函数用于唤醒等待队列中的全部线程。

      参数说明:

      • cond:唤醒在cond条件变量下等待的线程。

      返回值说明:

      • 函数调用成功返回0,失败返回错误码。

      使用示例:

      例如,下面我们用主线程创建三个新线程,让主线程控制这三个新线程活动。这三个新线程创建后都在条件变量下进行等待,直到主线程检测到键盘有输入时才唤醒一个等待线程,如此进行下去。

      #include <iostream>
      #include <cstdio>
      #include <pthread.h>
      
      pthread_mutex_t mutex;
      pthread_cond_t cond;
      void* Routine(void* arg)
      {
      	pthread_detach(pthread_self());
      	std::cout << (char*)arg << " run..." << std::endl;
      	while (true){
      		pthread_cond_wait(&cond, &mutex); //阻塞在这里,直到被唤醒
      		std::cout << (char*)arg << "活动..." << std::endl;
      	}
      }
      int main()
      {
      	pthread_t t1, t2, t3;
      	pthread_mutex_init(&mutex, nullptr);
      	pthread_cond_init(&cond, nullptr);
      	
      	pthread_create(&t1, nullptr, Routine, (void*)"thread 1");
      	pthread_create(&t2, nullptr, Routine, (void*)"thread 2");
      	pthread_create(&t3, nullptr, Routine, (void*)"thread 3");
      	
      	while (true){
      		getchar();
      		pthread_cond_signal(&cond);
      	}
      
      	pthread_mutex_destroy(&mutex);
      	pthread_cond_destroy(&cond);
      	return 0;
      }
      

      此时我们会发现唤醒这三个线程时具有明显的顺序性,根本原因是当这若干个线程启动时默认都会在该条件变量下去等待,而我们每次都唤醒的是在当前条件变量下等待的头部线程,当该线程执行完打印操作后会继续排到等待队列的尾部进行wait,所以我们能够看到一个周转的现象。
      Linux线程安全
      如果我们想每次唤醒都将在该条件变量下等待的所有线程进行唤醒,可以将代码中的pthread_cond_signal函数改为pthread_cond_broadcast函数。

      此时我们每一次唤醒都会将所有在该条件变量下等待的线程进行唤醒,也就是每次都将这三个线程唤醒。
      Linux线程安全
      小贴士: C++源文件的后缀可以是.cpp、.cc、.cxx。

      为什么pthread_cond_wait需要互斥量

      • 条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去都不会满足,所以必须要有一个线程通过某些操作,改变共享变量,使原先不满足的条件变得满足,并且友好的通知等待在条件变量上的线程。
      • 条件不会无缘无故的突然变得满足了,必然会牵扯到共享数据的变化,所以一定要用互斥锁来保护,没有互斥锁就无法安全的获取和修改共享数据。
      • 当线程进入临界区时需要先加锁,然后判断内部资源的情况,若不满足当前线程的执行条件,则需要在该条件变量下进行等待,但此时该线程是拿着锁被挂起的,也就意味着这个锁再也不会被释放了,此时就会发生死锁问题。
      • 所以在调用pthread_cond_wait函数时,还需要将对应的互斥锁传入,此时当线程因为某些条件不满足需要在该条件变量下进行等待时,就会自动释放该互斥锁。
      • 当该线程被唤醒时,该线程会接着执行临界区内的代码,此时便要求该线程必须立马获得对应的互斥锁,因此当某一个线程被唤醒时,实际会自动获得对应的互斥锁。

      总结一下:

      • 等待的时候往往是在临界区内等待的,当该线程进入等待的时候,互斥锁会自动释放,而当该线程被唤醒时,又会自动获得对应的互斥锁。
      • 条件变量需要配合互斥锁使用,其中条件变量是用来完成同步的,而互斥锁是用来完成互斥的。
      • pthread_cond_wait函数有两个功能,一就是让线程在特定的条件变量下等待,二就是让线程释放对应的互斥锁。

      错误的设计

      你可能会想:当我们进入临界区上锁后,如果发现条件不满足,那我们先解锁,然后在该条件变量下进行等待不就行了。

      //错误的设计
      pthread_mutex_lock(&mutex);
      while (condition_is_false){
      	pthread_mutex_unlock(&mutex);
      	//解锁之后,等待之前,条件可能已经满足,信号已经发出,但是该信号可能被错过
      	pthread_cond_wait(&cond);
      	pthread_mutex_lock(&mutex);
      }
      pthread_mutex_unlock(&mutex);
      

      但这是不可行的,因为解锁和等待不是原子操作,调用解锁之后,在调用pthread_cond_wait函数之前,如果已经有其他线程获取到互斥量,发现此时条件满足,于是发送了信号,那么此时pthread_cond_wait函数将错过这个信号,最终可能会导致线程永远不会被唤醒,因此解锁和等待必须是一个原子操作。

      而实际进入pthread_cond_wait函数后,会先判断条件变量是否等于0,若等于0则说明不满足,此时会先将对应的互斥锁解锁,直到pthread_cond_wait函数返回时再将条件变量改为1,并将对应的互斥锁加锁。

      条件变量使用规范

      等待条件变量的代码

      pthread_mutex_lock(&mutex);
      while (条件为假)
      	pthread_cond_wait(&cond, &mutex);
      修改条件
      pthread_mutex_unlock(&mutex);
      

      唤醒等待线程的代码

      pthread_mutex_lock(&mutex);
      设置条件为真
      pthread_cond_signal(&cond);
      pthread_mutex_unlock(&mutex);
      
      版权声明:本文内容来自第三方投稿或授权转载,原文地址:https://blog.csdn.net/chenlong_cxy/article/details/122657542,作者:2021dragon,版权归原作者所有。本网站转在其作品的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如因作品内容、版权等问题需要同本网站联系,请发邮件至ctyunbbs@chinatelecom.cn沟通。

      上一篇:Linux线程池

      下一篇:文件操作函数(四)—— 文件的结束判定

      相关文章

      2025-05-19 09:04:53

      查看RISC-V版本的gcc中默认定义的宏

      查看RISC-V版本的gcc中默认定义的宏

      2025-05-19 09:04:53
      c++ , linux
      2025-05-16 09:15:17

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17
      单例 , 线程 , 队列
      2025-05-14 10:07:38

      超级好用的C++实用库之互斥锁

      互斥锁是一种用于多线程编程的同步机制,其主要目的是确保在并发执行环境中,同一时间内只有一个线程能够访问和修改共享资源。

      2025-05-14 10:07:38
      CHP , Lock , 互斥 , 线程 , 释放 , 锁定
      2025-05-14 10:03:13

      超级好用的C++实用库之线程基类

      在C++中,线程是操作系统能够进行运算调度的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源,比如:内存空间和系统资源,但它们有自己的指令指针、堆栈和局部变量等。

      2025-05-14 10:03:13
      Linux , void , Windows , 函数 , 操作系统 , 线程
      2025-05-14 10:02:48

      互斥锁解决redis缓存击穿

      在高并发系统中,Redis 缓存是一种常见的性能优化方式。然而,缓存击穿问题也伴随着高并发访问而来。

      2025-05-14 10:02:48
      Redis , 互斥 , 数据库 , 线程 , 缓存 , 请求
      2025-05-14 09:51:15

      java怎么对线程池做监控

      对Java线程池进行监控是确保系统性能和稳定性的重要部分。监控线程池可以帮助我们了解线程池的状态,如当前活跃线程数、任务队列长度、已完成任务数等。

      2025-05-14 09:51:15
      Java , 方法 , 监控 , 示例 , 线程 , 队列
      2025-05-12 08:40:18

      如何向线程传递参数

      如何向线程传递参数

      2025-05-12 08:40:18
      传递 , 参数 , 封装 , 开启 , 线程
      2025-05-09 08:51:21

      notify和notifyall的区别

      notify和notifyall的区别

      2025-05-09 08:51:21
      notify , synchronized , 方法 , 线程 , 调用 , 释放
      2025-05-09 08:51:09

      Java之线程同步(同步方法、同步代码块)(关键字synchronized)(案例分析)

      多线程的并发执行可以提高程序的效率。但是多个线程访问共享资源时,会引发一些安全问题。

      2025-05-09 08:51:09
      代码 , 同步 , 执行 , 方法 , 线程
      2025-05-07 09:08:23

      Qt中线程的使用

      在qt中线程的使用有两种方式,第一种就是创建一个类继承QObject类,之后使用moveToThread函数将线程添加到类中。另一种就是创建一个类继承QThread类,在类中实现run函数。

      2025-05-07 09:08:23
      run , 使用 , 函数 , 线程
      查看更多
      推荐标签

      作者介绍

      天翼云小翼
      天翼云用户

      文章

      33561

      阅读量

      5227608

      查看更多

      最新文章

      Linux系统基础-多线程超详细讲解(5)_单例模式与线程池

      2025-05-16 09:15:17

      超级好用的C++实用库之互斥锁

      2025-05-14 10:07:38

      超级好用的C++实用库之线程基类

      2025-05-14 10:03:13

      互斥锁解决redis缓存击穿

      2025-05-14 10:02:48

      java怎么对线程池做监控

      2025-05-14 09:51:15

      如何向线程传递参数

      2025-05-12 08:40:18

      查看更多

      热门文章

      Java线程同步synchronized wait notifyAll

      2023-04-18 14:15:05

      程序员之路:Linux压缩命令

      2023-03-21 02:52:11

      Linux下查看进程线程数的方法

      2023-05-15 10:04:01

      Android Priority Job Queue (Job Manager):线程任务的容错重启机制(二)

      2024-09-25 10:13:46

      操作系统中的线程种类

      2023-04-24 11:27:18

      Android Priority Job Queue (Job Manager):多重不同Job并发执行并在前台获得返回结果(四)

      2023-04-13 09:54:33

      查看更多

      热门标签

      java Java python 编程开发 代码 开发语言 算法 线程 Python html 数组 C++ 元素 javascript c++
      查看更多

      相关产品

      弹性云主机

      随时自助获取、弹性伸缩的云服务器资源

      天翼云电脑(公众版)

      便捷、安全、高效的云电脑服务

      对象存储

      高品质、低成本的云上存储服务

      云硬盘

      为云上计算资源提供持久性块存储

      查看更多

      随机文章

      linux下查看线程数的方法及超过系统线程最大值的报错

      【Netty 】Netty 是什么?能做什么?

      表数据量大读写缓慢如何优化(1)【冷热分离】

      【Java】线程的同步和互斥锁

      send message failed [channel: Netty4TcpChanne GC (Allocation Failure,   ParNew Desired survivor size

      【Python】使用numpy库实现Tic-Tac-Toe井字棋

      • 7*24小时售后
      • 无忧退款
      • 免费备案
      • 专家服务
      售前咨询热线
      400-810-9889转1
      关注天翼云
      • 旗舰店
      • 天翼云APP
      • 天翼云微信公众号
      服务与支持
      • 备案中心
      • 售前咨询
      • 智能客服
      • 自助服务
      • 工单管理
      • 客户公告
      • 涉诈举报
      账户管理
      • 管理中心
      • 订单管理
      • 余额管理
      • 发票管理
      • 充值汇款
      • 续费管理
      快速入口
      • 天翼云旗舰店
      • 文档中心
      • 最新活动
      • 免费试用
      • 信任中心
      • 天翼云学堂
      云网生态
      • 甄选商城
      • 渠道合作
      • 云市场合作
      了解天翼云
      • 关于天翼云
      • 天翼云APP
      • 服务案例
      • 新闻资讯
      • 联系我们
      热门产品
      • 云电脑
      • 弹性云主机
      • 云电脑政企版
      • 天翼云手机
      • 云数据库
      • 对象存储
      • 云硬盘
      • Web应用防火墙
      • 服务器安全卫士
      • CDN加速
      热门推荐
      • 云服务备份
      • 边缘安全加速平台
      • 全站加速
      • 安全加速
      • 云服务器
      • 云主机
      • 智能边缘云
      • 应用编排服务
      • 微服务引擎
      • 共享流量包
      更多推荐
      • web应用防火墙
      • 密钥管理
      • 等保咨询
      • 安全专区
      • 应用运维管理
      • 云日志服务
      • 文档数据库服务
      • 云搜索服务
      • 数据湖探索
      • 数据仓库服务
      友情链接
      • 中国电信集团
      • 189邮箱
      • 天翼企业云盘
      • 天翼云盘
      ©2025 天翼云科技有限公司版权所有 增值电信业务经营许可证A2.B1.B2-20090001
      公司地址:北京市东城区青龙胡同甲1号、3号2幢2层205-32室
      • 用户协议
      • 隐私政策
      • 个人信息保护
      • 法律声明
      备案 京公网安备11010802043424号 京ICP备 2021034386号