Linux之线程安全(下)

news2024/11/16 3:18:28

文章目录

  • 前言
  • 一、Linux线程互斥
    • 1.mutex的理解
      • 原子性
      • 互斥锁实现原子性的原理
    • 2.mutex的封装——Mutex.hpp
    • 3.可重入和线程安全
      • 可重入
      • 线程安全
      • 线程安全不一定是可重入的,而可重入函数一定是线程安全的。
    • 4.死锁
      • 概念
      • 造成死锁的四个必要条件
      • 如何避免死锁
  • 二、Linux线程同步
    • 1.引入
    • 2.条件变量
    • 3.条件变量接口
    • 4.理解条件变量
    • 条件变量的使用
      • 一次唤醒一个线程
      • 一次唤醒一批线程
  • 总结


前言

本文承接上一篇文章的内容,继续介绍Linux中的线程安全问题及解决方法。


一、Linux线程互斥

1.mutex的理解

  1. 锁本身也是一个共享资源。
    共享资源需要被锁保护,但是锁本身也是共享资源,谁来保护锁呢?
    pthread_mutex_lock、pthread_mutex_unlock:加锁和解锁的过程必须是线程安全的,因此加锁的过程是原子的(未来解锁的一定是一个执行流)。
  2. 谁持有锁谁就能进入临界区。如果申请锁成功,就继续向后续代码执行。如果申请锁失败,执行流会怎么办?如果已经加了一次锁,然后再加一次锁,结果又会怎样?

这时,程序不再执行,执行流会阻塞。

原子性

如果线程1,申请锁成功,进入临界区,正在访问临界资源。此时其它进程真正阻塞等待。那么问题来了,这时该线程是否可以被切换?答案是肯定的,可以被切换。
当持有锁的线程被切换走时,它是抱着锁一起被切走的。即使该线程被切换掉,其它线程此时也无法申请锁,只能等待该线程将锁释放掉。
因此,对于其它线程而言,有意义的锁的状态只有两种:1.锁被申请前、2.锁被释放后。
在其它线程眼中,当前线程持有锁的过程就是原子的(要么持有,要么不持有)。

注意:

  1. 我们在使用锁的时候一定要尽量保证临界区的粒度尽可能小(粒度是加锁和解锁之间的代码的多少,即锁保护的代码的多少)。
  2. 加锁是程序员行为,必须做到要加就都加(公共资源,要么加锁,要么不加锁,这是程序员决定的,尽量避免因为锁而写bug)。

互斥锁实现原子性的原理

从汇编指令谈加锁:为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,它们的作用是把寄存器和内存单元的数据直接进行交换。由于该操作只用了一条指令,因此可以保证原子性。
汇编指令:

lock:
	movb $0, %al
	xchgb %al, mutex
	if(al寄存器的内容 > 0){
		return 0;
	}else
		挂起等待;
	goto lock;
unlock:
	movb $1, mutex
	唤醒等待Mutex的线程
	return 0;

加锁:将内存里的数据与寄存器中的数据进行交换,就加锁了。
在这里插入图片描述
xchgb指令将CPU中寄存器里的数据与内存中对应的数据进行直接交换(原子操作)
在这里插入图片描述
在这里插入图片描述
解锁:申请锁成功的线程将寄存器内的内容(上下文信息)与内存里的数据交换,然后就成功解锁了。

2.mutex的封装——Mutex.hpp

文件Mutex.hpp

  1 #pragma once
  2 #include<iostream>
  3 #include<pthread.h>
  4 using namespace std;
  5 class Mutex
  6 {
  7 public:
  8         Mutex(pthread_mutex_t* lock_p = nullptr)
  9         :lock_p_(lock_p)
 10         {}
 11         void lock()
 12         {
 13                 if(lock_p_)
 14                 {
 15                         pthread_mutex_lock(lock_p_);
 16                 }
 17         }
 18         void unlock()
 19         {
 20                 if(lock_p_)
 21                 {
 22                         pthread_mutex_unlock(lock_p_);
 23                 }
 24         }
 25         ~Mutex()
 26         {}
 27 private:
 28         pthread_mutex_t* lock_p;
 29 };
 30 
 31 class LockGuard
 32 {
 33 public:
 34         LockGroud(pthread_mutex_t* mutex)
 35         :mutex_(mutex)
 36         {
 37                 mutex_.lock();//在构造函数里加锁
 38         }
 39         ~LockGroud()
 40         {
 41                 mutex_.unlock();//在析构函数里解锁
 42         }
 43 private:
 44         Mutex mutex_;
 45 };

测试代码:
文件main.cc

  1 #include"Mutex.hpp"
  2 int tickets = 1000;
  3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  4 void* get_ticket(void* args)
  5 {
  6         string name = static_cast<const char*>(args);
  7         while(1)
  8         {
  9                 LockGuard lockguard(&lock);
 10                 if(tickets > 0)
 11                 {
 12                         usleep(1234);
 13                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;
 14                         tickets--;
 15                 }
 16                 else
 17                 {
 18                         break;
 19                 }
 20         }
 21         return nullptr;
 22 }
 23 int main()
 24 {
 25         pthread_t t1, t2, t3, t4;
 26         pthread_create(&t1, nullptr, get_ticket, (void*)"thread 1");
 27         pthread_create(&t2, nullptr, get_ticket, (void*)"thread 2");
 28         pthread_create(&t3, nullptr, get_ticket, (void*)"thread 3");
 29         pthread_create(&t4, nullptr, get_ticket, (void*)"thread 4");
 30 
 31         pthread_join(t1, nullptr);
 32         pthread_join(t2, nullptr);
 33         pthread_join(t3, nullptr);
 34         pthread_join(t4, nullptr);
 35         return 0;
 36 }

运行:
在这里插入图片描述

3.可重入和线程安全

可重入

同一个函数被不同的执行流调用,当前一个执行流还没有执行完,给予有其它执行流再次进入该函数,我们称为重入
一个函数在重入的状态下,运行结果不会出现任何不同或者没有出现任何问题,该函数被称为可重入函数。否则,该函数是不可重入函数

线程安全

线程安全:多个线程并发执行同一段代码,多次测试不会出现不同的结果(即,没有问题),常见的多线程对全局变量或静态变量进行操作,在没有锁保护的情况下会出现问题,例如:抢票。

线程安全不一定是可重入的,而可重入函数一定是线程安全的。

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

常见的可重入的情况
1.每个线程对全局变量或静态变量只有读取的权限,没有修改(写入)的权限,一般来说,这些线程是安全的;
2.类或者接口对于线程来说都是原子操作,多个线程之间的切换不会导致该接口的执行结果存在二义性。
常见的不可重入的情况
1.调用了malloc/free函数:因为malloc函数是用全局链表来管理堆的(链表的插入等操作是不可重入的);
2.调用标准I/O库函数:标准I/O库函的很多实现都是以不可重入的方式使用全局数据结构;
3.可重入函数体使用了静态的数据结构。

4.死锁

概念

一组执行流(进程/线程)持有自己锁资源的同时,还想申请对方的锁(但是,锁是不可抢占的,只能等持锁的线程主动归还),这会使得多个执行流互相等待对方持有的资源,导致代码无法推进。这就是死锁
特殊的,一把锁也会导致死锁问题,在已经申请锁的情况下,又去申请一把锁,就会导致死锁问题。

为什么会导致死锁?
前提是使用了锁——锁可以保护临界资源的安全;为啥要保护临界资源——多线程并发访问临界资源会导致数据不一致的问题——多线程的大部分资源是临界资源(共享资源)——多线程的特性决定的。
为了解决一个问题,带来了新的问题:死锁。任何技术都有自己的边界,在解决一个问题的同时,一定会导致另一个新的问题。

造成死锁的四个必要条件

  1. 互斥:一个共享资源每次仅被一个执行流使用;
  2. 请求和保持:一个执行流因请求其它资源而阻塞,同时也不释放已有资源;
  3. 不剥夺:一个执行流获得的资源在未(使用完毕)主动释放之前,不能被强行剥夺;
  4. 环路等待:执行流之间形成环路问题,循环等待资源。

如何避免死锁

  1. 破坏死锁的四个必要条件(破坏其中一个及以上即可)。
  2. 加锁顺序保持一致;
  3. 避免锁未释放的场景(防止出现锁一直被占有,无法申请);
  4. 资源一次性分配(一个执行流需要的资源,一次性全部分配给它)。

二、Linux线程同步

1.引入

举一些生活中的例子:
游乐园的热门项目,先到先玩;打印机打印东西,先到的人先打印;上厕所时将门反锁,其他人无法进入……
这些例子中,离资源越近的人竞争力越强,就导致一直是同一个人在拿到资源、释放资源,造成其他人饥饿状态。我们本节内容和上节内容所举的例子:抢票系统就是这样,我们发现很长一段时间一直是同一个线程在抢票,造成其它线程的饥饿问题。
为了解决这个问题,我们在数据安全的情况下让这些线程按照一定的顺序申请资源,这就是线程同步
饥饿状态:得不到锁资源,而无法访问公共资源的线程,处于饥饿状态。它并没有错,但是不合理。
竞态条件:因为时序问题导致程序异常,我们称为竞态条件。
线程同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步。

2.条件变量

当一个线程互斥的访问某个资源时,它有可能发现在其它线程改变状态之前,它不能对该资源进行操作。
例如:一个线程访问一个队列时,发现队列为空,它只能等待其它线程往该队列里添加节点,这种情况就需要用到条件变量。
条件变量通常是配合互斥锁一起使用的。
条件变量的使用:一个线程等待条件变量的条件成立而被挂起;另一个线程使条件成立后唤醒等待的线程。

3.条件变量接口

//初始化
int pthread_cont_init(pthread_cont_t* restrict cont, const pthread_condarr_t* restrict attr);
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t* cond);
//特定时间阻塞等待
int pthread_cond_timedwait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex, const struct timespec* restrict abstime);
//等待
int pthread_cond_wait(pthread_cond_t* restrict cond, pthread_mutex_t* restrict mutex);
//唤醒一批线程
int pthread_cond_broadcast(pthread_cond_t* cond);
//唤醒一个线程
int pthread_cond_signal(pthread_condt_t* cond);

4.理解条件变量

举例:公司进行招聘,很多应聘者来面试,只有一个面试官,一次只能有一个应聘者进入面试间进行面试。由于没有组织者进行组织,导致没有规则:上一个人面试完之后,所有人都拥挤到面试官面前申请面试,面试官只能选择离自己最近的那个人进行面试,这就导致一群人在外面等着,总有人抢不过别人一直没有面试机会,甚至有的人面试完一次再次申请面试的情况,造成其它人的饥饿问题。这种情况下,面试的效率很低下。
之后,面试官对面试的顺序制定了规则,设立了一个等待区,所有人按照到达的时间进行排队,这样一来所有人都有机会面试了。
而这个等待区就是条件变量,如果一个人想进行面试,就要先去等待区等待,未来所有的应聘者都要去条件变量等待。
在这里插入图片描述
条件不满足时,线程就必须去某些定义好的条件变量上进行等待
变量条件(struct cond, 结构体)里面包含状态、队列。条件变量里包含一个队列,不满足条件的线程就链接在这个队列上进行等待。
在这里插入图片描述

条件变量的使用

可以通过条件变量来控制线程的执行。由于条件变量本身并不具备互斥的功能,所以条件变量必须配合互斥锁使用:

一次唤醒一个线程

创建2个线程,通过条件变量一秒唤醒一个线程(或者全部唤醒)
文件test.cc

  1 #include"Mutex.hpp"
  2 int tickets = 1000;
  3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  4 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量
  5 void* start_routine(void* args)
  6 {
  7         string name = static_cast<const char*>(args);
  8         while(1)
  9         {
 10                 LockGuard lockguard(&lock);
 11                 pthread_cond_wait(&cond, &lock);
 12                 if(tickets > 0)
 13                 {
 14                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;
 15                         tickets--;
 16                 }
 17                 else
 18                 {
 19                         break;
 20                 }
 21         }
 22         return nullptr;
 23 }
 24 int main()
 25 {
 26         pthread_t t1, t2;
 27         pthread_create(&t1, nullptr, start_routine, (void*)"thread 1");
 28         pthread_create(&t2, nullptr, start_routine, (void*)"thread 2");
 29         while(1)
 30         {
 31                 sleep(1);
 32                 pthread_cond_signal(&cond);
 33                 cout<<"main thread wakeup one thread..."<<endl;
 34         }
 35         pthread_join(t1, nullptr);
 36         pthread_join(t2, nullptr);
 37         return 0;
 38 }

在这里插入图片描述

主线程一个一个去叫,按照一定顺序输出打印。

一次唤醒一批线程

文件test1.cc

  1 #include"Mutex.hpp"
  2 int tickets = 1000;
  3 pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
  4 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//条件变量
  5 void* start_routine(void* args)
  6 {
  7         string name = static_cast<const char*>(args);
  8         while(1)
  9         {
 10                 LockGuard lockguard(&lock);
 11                 pthread_cond_wait(&cond, &lock);
 12                 if(tickets > 0)
 13                 {
 14                         cout<<name<<"正在抢票, 剩余票数:"<<tickets<<endl;
 15                         tickets--;
 16                 }
 17                 else
 18                 {
 19                         break;
 20                 }
 21         }
 22         return nullptr;
 23 }
 24 int main()
 25 {
 26         pthread_t t1, t2, t3, t4, t5;
 27         pthread_create(&t1, nullptr, start_routine, (void*)"thread 1");
 28         pthread_create(&t2, nullptr, start_routine, (void*)"thread 2");
 29         pthread_create(&t3, nullptr, start_routine, (void*)"thread 3");
 30         pthread_create(&t4, nullptr, start_routine, (void*)"thread 4");
 31         pthread_create(&t5, nullptr, start_routine, (void*)"thread 5");
 32         while(1)
 33         {
 34                 sleep(1);
 35                 pthread_cond_broadcast(&cond);
 36                 cout<<"main thread wakeup one thread..."<<endl;
 37         }
 38         pthread_join(t1, nullptr);
 39         pthread_join(t2, nullptr);
 40         return 0;
 41 }

运行:
在这里插入图片描述


总结

以上就是今天要讲的内容,本文继上一篇文章继续介绍了线程安全的相关内容,主要介绍了锁以及条件变量等相关概念。作者目前也是正在学习Linux相关的知识,如果文章中的内容有错误或者不严谨的部分,欢迎大家在评论区指出,也欢迎大家在评论区提问、交流。
最后,如果本篇文章对你有所启发的话,希望可以多多支持作者,谢谢大家!

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

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

相关文章

Mendix低代码开发

Mendix低代码开发 目录概述需求&#xff1a; 设计思路实现思路分析1.URL管理2.LL3.Mendix 低代码可视化开发4.Mendix 低代码可视化开发 参考资料和推荐阅读 Survive by day and develop by night. talk for import biz , show your perfect code,full busy&#xff0c;skip har…

许多智能算法并不智能

数学的精髓在于不断寻找简洁而优美的解决方法&#xff0c;而智能的精髓也在于尽可能地避免繁琐的计算&#xff0c;通过更高效的方式来解决问题。从实践角度看&#xff0c;现代人工智能技术的发展&#xff0c;正是基于这个思路不断推进的。在机器学习领域中&#xff0c;人们通过…

【C语言复习】第二篇、VS2017软件的使用以及常用小技巧

目录 1、VS2017软件无法打开stdio.h文件的解决办法 2、VS2017软件解决scanf函数问题以及如何建立初始模板 2.1、visual Studio使用scanf函数出现报错问题 2.2、如何实现新建一个.c文件就有初始模板 3、VS2017软件如何显示代码行号&#xff1f; 4、VS2017软件如何快速复制…

Redis(Windows版)安装

Redis安装过程 目前只是Windows下安装&#xff0c;后续会添加linux下安装过程 Windows安装 下载地址&#xff1a;Releases tporadowski/redis (github.com) Redis安装要根据系统平台的实际情况而定&#xff0c;我使用的是免安装的 下载完成&#xff0c;解压之后打开文件夹…

怎么计算 flex-shrink 的缩放尺寸

计算公式: 子元素的宽度 - (子元素的宽度的总和 - 父盒子的宽度) * (某个元素的flex-shrink / flex-shrink总和) 面试问题是这样的下面 left 和 right 的宽度分别是多少 * {padding: 0;margin: 0;}.container {width: 500px;height: 300px;display: flex;}.left {width: 500px…

MySQL8.0安装过程中starting the server报错的解决方案(史上最详细)

MySQL8.0安装过程中starting the server报错的解决方案&#xff08;史上最详细&#xff09; 目录 MySQL8.0安装过程中starting the server报错的解决方案&#xff08;史上最详细&#xff09;报错情况&#xff1a;starting the server报错解决办法 报错情况&#xff1a;starting…

盘点开源ChatGPT建立的私有知识库

ChatGPT 可以落地的一个行业就是建立私有知识库&#xff0c;将ChatGPT落地TO B行业&#xff0c;可基于ChatGPT和私有数据构建智能知识库和个性化AI。 这个应该是ChatGPT 最热的一个创业方向。 可能出现的产品&#xff0c;有智能AI客服、企业内部/外部知识库、个人知识库&…

C++(9):顺序容器

顺序容器概述 所有顺序容器都提供了快速顺序访问元素的能力。 vector//可变大小数组。支持快速随机访问。在尾部之外的位置插入或删除元素可能很慢 deque//双端队列。支持快速随机访问。在头尾位置插入/删除速度很快 list//双向链表。只支持双向顺序访问。在list中任何位置进…

oVirt 4.4.10三节点超融合集群安装配置及集群扩容(二)

在上节安装完成3节点集群后&#xff0c;在此基础上扩容到6节点<oVirt每次扩容后的容量必须是3的倍数> 操作步骤 在原始第一台服务器访问https://192.168.5.100:9090/,在"Virtualiztion"->“Hosted Engin"下点击"Manage Gluster” 点击"Ex…

「已解决」已有Umi Antd 环境下安装 formily v2 依赖报错问题

背景 在一个项目中想引入 formily v2 试一下这个针对复杂表单的解决方案&#xff0c;结果发现安装后报错&#xff0c;目前已有的第三方库大致为 “ant-design/icons”: “^5.0.1”, “ant-design/pro-components”: “^2.4.4”, “umijs/max”: “^4.0.68”, “ahooks”: “^3…

【软考程序员学习笔记】——操作系统

目录 &#x1f34a;一、操作系统的五大功能 &#x1f34a;二、操作系统的分类 &#x1f34a;三、进程三态模型 &#x1f34a;四、信号量机制 &#x1f34a;五、PV机制、互斥和同步 &#x1f34a;六、虚拟存储器 &#x1f34a;七、设备管理 直接程序控制 &#x1f34a;八…

PyQt5桌面应用开发(21):界面设计结果自动测试(二)

本文目录 PyQt5桌面应用系列TDDUI为什么&#xff1f; 开发任务任务设计小码的工作unittest函数一览表 UI单元测试代码控件代码测试报告 总结 PyQt5桌面应用系列 PyQt5桌面应用开发&#xff08;1&#xff09;&#xff1a;需求分析 PyQt5桌面应用开发&#xff08;2&#xff09;&a…

Python基础(2)——Python解释器

Python基础&#xff08;2&#xff09;——Python解释器 文章目录 Python基础&#xff08;2&#xff09;——Python解释器目标一. 解释器的作用二. 下载Python解释器三. 安装Python解释器总结 目标 解释器的作用下载Python解释器安装Python解释器 一. 解释器的作用 Python解释…

Golang每日一练(leetDay0099) 单词规律I\II Word Pattern

目录 290. 单词规律 Word Pattern &#x1f31f;  291. 单词规律 II Word Pattern ii &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Python每日一练 专栏 C/C每日一练 专栏 Java每日一练 专栏 …

ubuntu 20.04 arm 平台交叉编译 glib 库

glib 是什么&#xff1f; glib 是一个比较强大的 软件库&#xff0c;类似于 libc 库 交叉编译 当前需要移植到 arm&#xff08;ARM 32位&#xff09;平台上&#xff0c;需要使用 arm 交叉编译工具链编译 glib 环境准备 ubuntu 20.04 安装较新版本的 meson &#xff1a; &g…

手写-js节流(定时器+时间差两种方式)

官方解释&#xff1a;当持续触发事件时&#xff0c;保证一定时间段内只调用一次事件处理函数。 节流实现思路: 实现节流函数, 我们使用定时器是不方便管理的, 实现节流函数我们采用另一个思路 我们获取一个当前时间nowTime, 我们使用new Date().gettime()方法获取, 在设定一个…

[RPC]:Feign远程调用

文章目录 1 RPC框架-Feign1.1 什么是Feign1.2 Feign解决的问题1.2.1 使用RestTemplate发送远程调用代码1.2.2 存在的问题 1.3 Feign如何使用1.3.1 引入依赖 1 RPC框架-Feign 1.1 什么是Feign Feign是一个简化HTTP客户端编写的框架&#xff0c;通过声明式方式将远程服务调用封装…

基于html+css的图展示131

准备项目 项目开发工具 Visual Studio Code 1.44.2 版本: 1.44.2 提交: ff915844119ce9485abfe8aa9076ec76b5300ddd 日期: 2020-04-16T16:36:23.138Z Electron: 7.1.11 Chrome: 78.0.3904.130 Node.js: 12.8.1 V8: 7.8.279.23-electron.0 OS: Windows_NT x64 10.0.19044 项目…

Three.js教程:平行光与环境光

推荐&#xff1a;将 NSDT场景编辑器 加入你的3D工具链 其他系列工具&#xff1a; NSDT简石数字孪生 平行光与环境光 本节课通过平行光DirectionalLight (opens new window)和环境光AmbientLight (opens new window)进一步了解光照对应模型Mesh表面的影响。 点光源辅助观察Poin…

管理类联考——英语——技巧篇——新题型——经典方法论

新题型可以说是考研英语独有的考查形式&#xff0c;自2005年起&#xff0c;阅读理解部分便新增了PartB&#xff0c;即新题型。之所以叫作新题型&#xff0c;原因很简单&#xff0c;是因为它在考研英语的各个题型中&#xff0c;年龄是最小的&#xff0c;也就是最新的题型。新题型…