Linux之【多线程】线程互斥(锁)线程同步(条件变量)

news2024/11/24 1:57:36

Linux之【多线程】线程互斥(锁)&线程同步(条件变量)

  • 一、引入:线程安全问题
  • 二、浅谈"++"和"- -"非原子性操作
  • 三、Linux线程互斥
    • 3.1 互斥量-->mutex⚠️
      • 3.1.1 互斥锁的理解
      • 3.1.2 深入了解锁的原子性⚠️
    • 3.2 线程安全与可重入函数
  • 四、死锁
  • 五、Linux线程同步
    • 5.1 初步认识
    • 5.2 条件变量⚠️
    • 5.3 结合生活理解条件变量
    • 5.4 结合代码简单理解条件变量

文章篇幅较长,请耐心阅读😀😀😀😀😀😀

一、引入:线程安全问题

全局变量可以被多个线程同时访问,多个线程对其进行操作,可能会出现数据不一致问题。

下面以一个购票池为例:

int tickets = 1000;//共享资源
void* get_ticket(void* args)
{
    std::string name = static_cast<const char *>(args);
    while(true)
    {
        if(tickets>0)
        {
            usleep(1111);//引起线程阻塞,挂起,切换其他线程
            cout<<name<<"正在抢票 : "<<tickets<<endl;
            tickets--;
        }
        else
        {
            break;
        }
    }
    return nullptr;
}
int main()
{
    std::unique_ptr<Thread> thread1(new Thread(get_ticket,(void*)"User1",1));
    std::unique_ptr<Thread> thread2(new Thread(get_ticket,(void*)"User2",2));
    std::unique_ptr<Thread> thread3(new Thread(get_ticket,(void*)"User3",3));
    std::unique_ptr<Thread> thread4(new Thread(get_ticket,(void*)"User4",4));

    thread1->join();
    thread2->join();
    thread3->join();
    thread4->join();
    return 0;
}

在这里插入图片描述
观察图片可以发现,票数有负数!!!

  • 多个线程交叉运行,即让调度器频繁地发生线程调度与切换 ,线程一般在时间片结束、来了更高级别的线程、线程等待的时候发生切换

  • 线程等待:当从内核态返回用户态的实施,线程要对调度状态监测,如果可以,就发生切换;检测工作是由OS来做的,但是线程共享地址空间,执行OS的代码本来就是在线程上下文执行,3–4G是内核代码,当线程检测,只不过执行OS代码,实际上就是OS在检测

  • 上述代码极端情况在tickets==1时,假设所有线程都进去,然后第一个线程在判断:(1.读取内存数据cpu内的寄存器中2.进行判断),为真进入代码块,这个时候发生线程切换并带走上下文数据,其余线程依次进行判断并执行和第一个线程一样的动作,直到第一个线程被唤醒,执行减1并写回内存,这个时候tickets已经为0,但是其余线程还没有结束,也会执行减减并修改数据,导致出现负数的情况

  • 减减的本质就是1.读取数据2.更改数据3.写回数据

二、浅谈"++“和”- -"非原子性操作

对变量进行++或者–,在C、C++上看起来只有一条语句,但是汇编之后至少是三条语句:
1.从内存读取数据到CPU寄存器中
2.在寄存器中让CPU进行对应的算逻运算
3.写回新的结果到内存中变量的位置
在这里插入图片描述

  • 现在线程A把数据加载到寄存器中,做减减,成为99,到第三步的时候写回到内存的时候被切走了,顺便把寄存器中的上下文也拿走了:

在这里插入图片描述

  • 此时调度线程B,一直在减减,当tickets变为10的时候,内存中变量的也变为了10,但是当它想继续减减的时候,线程B被切走了,带着自己的上下文走了

在这里插入图片描述

  • 现在线程A回来了:恢复寄存器上下文,继续之前的第三步,线程B已经把tickets变为10,但是被线程A改为了99!!!

在这里插入图片描述
由此可知我们定义的全局变量在没有保护的时候,往往是不安全的,像上面的例子,多个线程交替执行时造成数据安全问题,发生了数据不一致问题。

而解决这种问题的办法就是加锁

三、Linux线程互斥

临界资源:多个执行流进行安全访问的共享资源就叫临界资源

临界区:多个执行流进行访问临界资源的代码就是临界区

互斥: 任何时刻,互斥保证有且只有一个执行流进入临界区,访问临界资源,通常对临界资源起保护作用。

原子性: 不会被任何调度机制打断的操作,该操作只有两态,要么不做,要么做完,这就是原子性。

现在结合上文,先"简单"理解原子性:一个资源进行的操作如果只用一条汇编语句就能完成,就是原子性的,反之不是原子的。(++ --就不是原子性的),文章后面会再详解

3.1 互斥量–>mutex⚠️

3.1.1 互斥锁的理解

#include <pthread.h>
// 局部锁初始化
int pthread_mutex_init(pthread_mutex_t *restrict mutex,const pthread_mutexattr_t *restrict attr);
// 销毁
int pthread_mutex_destroy(pthread_mutex_t *mutex);
// 全局锁初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//成功返回0,失败返回错误码


#include <pthread.h>
//加锁
int pthread_mutex_lock(pthread_mutex_t *mutex);

//如果加锁成功,直接持有锁,加锁不成功,此时立马出错返回(试着加锁,非阻塞获取方式)
int pthread_mutex_trylock(pthread_mutex_t *mutex);

//解锁
int pthread_mutex_unlock(pthread_mutex_t *mutex);
// 成功返回0,失败返回错误码


加上局部互斥锁的售票池

class ThreadData
{
public:
    ThreadData(const std::string threadname, pthread_mutex_t *mutex_p)
        : threadname_(threadname), mutex_p_(mutex_p)
    {
    }
    ~ThreadData() {}

public:
    std::string threadname_;
    pthread_mutex_t *mutex_p_;
};
int tickets = 100; // 共享资源--临界资源
void *get_ticket(void *args)
{
    ThreadData *td = static_cast<ThreadData *>(args);
    while (true)
    {
        pthread_mutex_lock(td->mutex_p_);
        /* */ if (tickets > 0)
        /*临*/ {
        /*  */      usleep(1111); // 引起线程阻塞,挂起,切换其他线程
        /*界*/      cout << td->threadname_ << "正在抢票 : " << tickets << endl;
        /*  */      tickets--;
        /*区*/      pthread_mutex_unlock(td->mutex_p_);
                }
        else
        {
            pthread_mutex_unlock(td->mutex_p_);
            break;
        }
         //usleep(1000);//休息一会,让别的线程申请锁
    }
    return nullptr;
}
int main()
{
#define NUM 4
    pthread_mutex_t lock;
    pthread_mutex_init(&lock, nullptr);
    vector<pthread_t> tids(NUM);
    for (int i = 0; i < NUM; ++i)
    {
        char buffer[1024];
        snprintf(buffer, sizeof buffer, "thread %d", i + 1);
        ThreadData *td = new ThreadData(buffer, &lock);
        pthread_create(&tids[i], nullptr, get_ticket, td);
    }
    for(const auto& tid:tids)
    {
        pthread_join(tid,nullptr);
    }
    return 0;
}    

在这里插入图片描述
此时的运行结果每次都是能够减到1,且不是负数,但是运行的速度也变慢了。这是因为加锁和解锁的过程是多个线程串行执行的,程序变慢了

同时这里看到每次都是只有一个线程在抢票,这是因为锁只规定互斥访问,并没有规定谁来优先执行,所以谁的竞争力强就谁来持有锁

只需要取消//usleep(1000)注释即可
在这里插入图片描述
全局锁的使用
比局部简单,只需要在全局内初始化,不需要init、destroy就可以直接使用

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

小结:锁的概念⚠️

  1. 锁本身就是一个共享资源!全局的变量是要被保护的,锁是用来保护全局的资源的,锁本身也是全局资源
  2. pthread_mutex_lock、pthread_mutex_unlock:加锁和解锁的过程必须是安全的!加锁的过程其实是原子的
  3. 如果申请锁暂时没有成功,执行流暂时阻塞,直到有人释放锁
  4. 谁先拿到锁,谁先进入临界区

3.1.2 深入了解锁的原子性⚠️

针对锁的原子性概念
在这里插入图片描述
锁是原子性的原理
从汇编谈加锁:为了实现互斥锁操作,大多数体系结构提供了swap和exchange指令,作用是把寄存器和内存单元的数据直接做交换,由于只用一条指令,就可以保证原子性
在这里插入图片描述

  1. 线程A申请锁:把0move到寄存器中,然后交换数据,%al里面变成1,内存里面变成0,之后,被切走,需要携带自己的上下文数据一起跑路!!!
    在这里插入图片描述

  2. 线程B前来申请锁资源,把0写进%al里面,也是要交换数据,但是执行判断条件的时候为假,需要挂起等待。

  3. 这个时候,线程A结束阻塞,恢复上下文数据并接着执行上次未执行完的代码,判断为真,return 0,申请锁成功。

解锁:过程很简单,把寄存器的内容1移动到内存中,直接return,解锁完成

3.2 线程安全与可重入函数

线程安全:多个线程并发同一段代码时,不会出现不同的结果。常见对全局变量或者静态变量进行操作,并且没有锁保护的情况下,会出现该问题。

重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数
可重入与线程安全区别

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

四、死锁

死锁概念:一组执行流(不管进程还是线程)持有自己锁资源的同时,还想要申请对方的锁,锁是不可抢占的(除非自己主动归还),会导致多个执行流互相等待对方的资源,而导致代码无法推进。这就是死锁
注:一把锁可以造成死锁,先申请一把锁,未释放再申请一把锁
死锁四个必要条件:
1.互斥:一个共享资源每次被一个执行流使用

2.请求与保持:一个执行流因请求资源而阻塞,对已有资源保持不放

3.不剥夺:一个执行流获得的资源在未使用完之前,不能强行剥夺

4.环路等待条件:执行流间形成环路问题,循环等待资源

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

五、Linux线程同步

5.1 初步认识

引入情景:上面的抢票系统我们看到一个线程一直连续抢票,造成了其他线程的饥饿,为了解决这个问题:我们在数据安全的情况下让这些线程按照一定的顺序进行访问,这就是线程同步

饥饿状态:得不到锁资源而无法访问公共资源的线程处于饥饿状态。但是并没有错,但是不合理

竞态条件:因为时序问题,而导致程序异常,我们称为竞态条件。

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

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

5.2 条件变量⚠️

  1. 当一个线程互斥地访问某个变量时,它可能发现在其它线程改变状态之前,它什么也做不了。
  2. 例如一个线程访问队列时,发现队列为空,它只能等待,只到其它线程将一个节点添加到队列中。这种情况就需要用到条件变量

函数接口认识

#include <pthread.h>
//初始化
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
//全局初始化
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
//销毁
int pthread_cond_destroy(pthread_cond_t *cond);
------------------------------------------------------
//阻塞等待
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_cond_t *cond);
   

5.3 结合生活理解条件变量

应聘者要面试,不能同时进入房间进行面试,但是没有由于没有组织,上一个人面试完之后,面试官打开门准备面试下一个,一群人在外面等待面试,但是有人抢不过别人,面试官存在记不住谁面试过了,所以有可能一个人面试完之后又去面试了,造成其他人饥饿问题,这时候效率很低

后来重新进行管理:设立一个等待区,所有人都在这里等待并由面试官安排进入,等待区+面试官就组成了条件变量;如果一个人想面试,先得去排队并在等待区等待,未来所有应聘者都要等

在这里插入图片描述

条件变量(struct cond)里面包含状态,队列,而我们定义好的条件变量包含一个队列,不满足条件的线程就链接在这个队列上进行等待
在这里插入图片描述

5.4 结合代码简单理解条件变量

每次唤醒一个线程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
using namespace std;


int tickets = 1000;
//初始化全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//初始化全局变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//线程阻塞挂起
        //判断暂时省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
    pthread_t t1,t2;
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 1");
    pthread_create(&t1,nullptr,start_routine,(void*)"thread 2");

    while(true)
    {
        sleep(1);
        pthread_cond_signal(&cond);//随机唤醒一个等待的线程
        cout<<"main thread wakeup one thread..."<<endl;
    }
    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);

    return 0;
}

在这里插入图片描述
一次唤醒全部线程

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <vector>
using namespace std;


int tickets = 1000;
//初始化全局锁
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//初始化全局变量
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
void* start_routine(void* args)
{
    string name = static_cast<const char*>(args);
    while(true)
    {
        pthread_mutex_lock(&mutex);
        pthread_cond_wait(&cond,&mutex);//线程阻塞挂起
        //判断暂时省略
        cout<<name<<" -> "<<tickets<<endl;
        tickets--;
        pthread_mutex_unlock(&mutex);
    }
}
int main()
{
#define NUM 5
    vector<pthread_t> tids(NUM);
    for(int i=0;i<NUM;++i)
    {
        char* namebuffer=new char[1024];
        snprintf(namebuffer,1024,"thread->%d",i+1);
        pthread_create(&tids[i],nullptr,start_routine,namebuffer);
    }
    while(true)
    {
        sleep(1);
        pthread_cond_broadcast(&cond);//唤醒全部等待的线程
        cout<<"main thread wakeup all thread..."<<endl;
    }
    for(const auto& tid:tids)
    {
        pthread_join(tid,nullptr);
    }
    return 0;
}

在这里插入图片描述

关于线程同步的暂时讲到这里,后面会结合生产者消费者模型细细讲解一番!!!
附:本文Thread.hpp和Mutex.hpp皆在我的码云

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

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

相关文章

倾斜摄影超大场景的三维模型的顶层合并的优势浅析

倾斜摄影超大场景的三维模型的顶层合并的优势浅析 倾斜摄影超大场景的三维模型的顶层合并具有以下优势&#xff1a; 1、三维可视化效果好&#xff1a;通过倾斜摄影技术可以获得高分辨率的地面影像&#xff0c;将其与三维建模相结合可以生成非常逼真的三维场景。这种高度可视化…

SpringBoot事务管理-5个面试核心类源码刨析

“简单的事重复做&#xff0c;你就是专家&#xff1b;重复的事用心做&#xff0c;你就是赢家。” 在开始讲解SpringBoot事务之前&#xff0c;我们先来整体回顾下事务的概念及特性&#xff0c;便于我们了解SpringBoot是如何解决事务相关问题的&#xff0c;另外这部分也是面试必…

Windows安装配置Tomcat服务器教程 - 外网远程访问

文章目录 前言1.本地Tomcat网页搭建1.1 Tomcat安装1.2 配置环境变量1.3 环境配置1.4 Tomcat运行测试1.5 Cpolar安装和注册 2.本地网页发布2.1.Cpolar云端设置2.2 Cpolar本地设置 3.公网访问测试4.结语 转载自cpolar文章&#xff1a;外网访问本地Tomcat服务器【cpolar内网穿透】…

Cadence (1) 手动制作SMD封装

前提&#xff1a;软件版本 焊盘设计 &#xff1a;Pad Designer16.6PCB设计 &#xff1a;PCB Editor16.6PCB参考&#xff1a;LP Viewer 10.2 文章目录 SMD封装制作(R0603)封装信息SMD焊盘制作新建工程添加焊盘库路径到PCB EditorPCB Editor设计预处理放置焊盘放置丝印放置1脚标识…

【10.HTML入门知识-CSS元素定位】

1 标准流&#xff08;Normal Flow&#xff09; 默认情况下&#xff0c;元素都是按照normal flow&#xff08;标准流、常规流、正常流、文档流【document flow】&#xff09;进行排布  从左到右、从上到下按顺序摆放好  默认情况下&#xff0c;互相之间不存在层叠现象 1.1…

【13.HTML-动画】

1 CSS属性 - transform 1.1 位移 - translate translate的百分比可以完成一个元素的水平和垂直居中&#xff1a; 1.2 缩放 - scale 1.3 旋转 - rotate 1.4 transform-origin 形变的原点 1.5 倾斜 - skew 1.6 transform设置多个值 2 transition动画 2.1 认识transition动画 2…

Java反射(原理剖析与使用)

一、反射机制是什么 1、Java反射机制的核心是在程序运行时动态加载类并获取类的详细信息&#xff0c;从而操作类或对象的属性和方法。本质是JVM得到class对象之后&#xff0c;再通过class对象进行反编译&#xff0c;从而获取对象的各种信息。 2、Java属于先编译再运行的语言&a…

2023年第二十届五一数学建模竞赛C题:“双碳”目标下低碳建筑研究-思路详解与代码答案

该题对于模型的考察难度较低&#xff0c;难度在于数据的搜集以及选取与处理。 这里推荐数据查询的网站&#xff1a;中国碳核算数据库&#xff08;CEADs&#xff09; https://www.ceads.net.cn/ 国家数据 国家数据​data.stats.gov.cn/easyquery.htm?cnC01 以及各省市《统…

第四届“长城杯”信息安全铁人三项赛决赛RE-obfuscating

这里主要是加了混淆 这里要用到IDA的一个插件D810和去混淆脚本deflat.py。值得注意的是deflat.py无法在主逻辑去混淆&#xff0c;这里可以参考这篇文章的脚本利用angr符号执行去除控制流平坦化 - 0x401RevTrain-Tools (bluesadi.github.io)。在使用deflat.py和这文章中的脚本轮…

【AI折腾录】stable web ui基础【sd安装、lora vae embedding hyperwork等基础概念】

目录 一 sd安装二 目标三 sd基础3.1 模型3.2 vae&#xff08;Variational autoencoder&#xff0c;变分自编码器&#xff09;3.3 embedding3.3.1 安装方式3.3.2 使用方式 3.4 Lora3.4.1 lora组成3.4.2 使用&#xff1a;3.4.3 效果3.4.4 测试不同CFG效果 3.5 hypernetworks 超网…

LeetCode_BFS_DFS_中等_1376.通知所有员工所需的时间

目录 1.题目2.思路3.代码实现&#xff08;Java&#xff09; 1.题目 公司里有 n 名员工&#xff0c;每个员工的 ID 都是独一无二的&#xff0c;编号从 0 到 n - 1。公司的总负责人通过 headID 进行标识。 在 manager 数组中&#xff0c;每个员工都有一个直属负责人&#xff0c…

UE5实现距离测量功能

文章目录 1.实现目标2.实现过程2.1 Widget2.2 蓝图实现3.参考资料1.实现目标 UE5在Runtime环境下测量两个空间点位之间的绝对距离,并支持多段线的距离测量,GIF动图如下所示: 2.实现过程 实现原理比较简单,首先是基于PDI绘制线,有关绘制点和绘制线的可以看本专栏之前的文章…

css弹性布局

目录 1、实现弹性布局的前提&#xff1a;给父元素设置display:flex; 2、flex-direction&#xff1a;确定主轴方向 3、flex-wrap&#xff1a;是否换行 4、justify-content&#xff1a;主轴对齐方式 5、align-items&#xff1a;交叉轴对齐方式 6、align-content&#xff1a…

AWSFireLens轻松实现容器日志处理

applog应用程序和fluent-bit共享磁盘&#xff0c;日志内容是json格式数据&#xff0c;输出到S3也是JSON格式 applog应用部分在applog目录&#xff1a; Dockerfile文件内容 FROM alpine RUN mkdir -p /data/logs/ COPY testlog.sh /bin/ RUN chmod 777 /bin/testlog.sh ENTRYP…

人工智能技术在建筑能源管理中的应用场景

人工智能技术在建筑能源管理中的应用场景&#xff08;龙惟定&#xff09;&#xff0c;2021 摘 要 本文简要介绍了建筑能源管理(building energy management, BEM) 的概念。并从5个方面阐述了 BEM 对人工智能(AI) 技术的需求&#xff0c;即楼宇控制需要由从顶到底的基于物理模…

03-Vue技术栈之生命周期

目录 1、什么是生命周期2、分析生命周期2.1 生命周期钩子函数2.2 生命周期钩子函数的作用2.3 生命周期钩子函数图例2.4 生命周期钩子函数的应用 3、生命周期总结 1、什么是生命周期 又名&#xff1a;生命周期回调函数、生命周期函数、生命周期钩子。是什么&#xff1a;Vue在关…

ChatGPT实现编程语言转换

编程语言转换 对于程序员来说&#xff0c;往往有一类工作&#xff0c;是需要将一部分业务逻辑实现从服务端转移到客户端&#xff0c;或者从客户端转移到服务端。这类工作&#xff0c;通常需要将一种编程语言的代码转换成另一种编程语言的代码&#xff0c;这就需要承担这项工作…

Java多线程深入探讨

1. 线程与进程2. 创建和管理线程2.1. 继承Thread类2.2. 实现Runnable接口2.3 利用Callable、FutureTask接口实现。2.4 Thread的常用方法 3. 线程同步3.1. synchronized关键字3.1.1同步代码块&#xff1a;3.1.2 同步方法&#xff1a; 3.2. Lock接口 4. 线程间通信5. 线程池5.1 使…

vue - pc端实现对div的拖动功能

实现对div的拖动功能&#xff0c;需要先要知道以下的一些原生事件和方法&#xff1b; 1&#xff0c;事件: 方法描述onmousedown鼠标按钮被按下onmousemove鼠标被移动onmouseup鼠标按键被松开 2&#xff0c;方法: 方法描述event.clientX返回当事件被触发时鼠标指针相对于浏览…

【BIM+GIS】Supermap加载实景三维倾斜摄影模型

OSGB是常见的倾斜模型格式,本文讲述如何在Supermap中加载实景三维倾斜摄影模型OSGB。 文章目录 一、生成配置文件二、加载倾斜模型1. 新建场景2. 添加模型3. 高程调整一、生成配置文件 点击【三维数据】→【数据管理】→【生成配置文件】。 参数设置如下: 源路径:选择倾斜模…