Linux下线程的互斥与同步详解

🤖个人主页:晚风相伴-CSDN博客

💖如果觉得内容对你有帮助的话,还请给博主一键三连(点赞💜、收藏🧡、关注💚)吧

🙏如果内容有误或者有写的不好的地方的话,还望指出,谢谢!!!

让我们共同进步

下一篇《生产者消费者模型》敬请期待

目录

🔥线程间互斥的相关概念

💪互斥量的接口

初始化互斥量

销毁互斥量

互斥量的加锁与解锁

🔥探究互斥量实现原理

可重入函数和线程安全 

两者的概念区分 

常见的线程不安全和安全情况

可重入与线程安全的联系与区别

☀死锁 

产生死锁的四个必要条件

避免死锁

🔥线程同步 

条件变量 

同步的概念与竞态条件

🔥条件变量接口

初始化 

销毁条件 

条件等待

唤醒等待

🔥解释pthread_cond_wait中的互斥量


🔥线程间互斥的相关概念

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

先来看看下面简单实现的抢票的代码

int tickets = 1000;

 void* getTickets(void* args)
 {
     (void)args;
     while(true)
     {
         if(tickets > 0)
         {
             usleep(1000);
             printf("%p: %d\n", pthread_self(), tickets);
             tickets--;
         }
         else
         {
            break;
         }
     }
     return nullptr;
 }

 int main()
 {
     pthread_t t1, t2, t3;
     pthread_create(&t1, nullptr, getTickets, nullptr);
     pthread_create(&t1, nullptr, getTickets, nullptr);
     pthread_create(&t1, nullptr, getTickets, nullptr);

     pthread_join(t1, nullptr);
     pthread_join(t2, nullptr);
     pthread_join(t3, nullptr);

     return 0;
 }

结果演示

💪为什么结果会出现-1呢?

原因:首先要知道一个线程什么时候被调度,调度多长时间,完全是有计算机确定的,程序员决定不了。tickets在进行减减操作时,是分三步的

①读取数据到CPU内的寄存器中

②CPU内部进行计算--

③将结果写回内存中

为了方便叙述,这里给线程编个号

一号线程来了,由于时间片很短执行到第②步就被切走了,二号线程来了,它没有被打断,所以它执行完了这三步,并且这个线程的优先级比较高,一直执行tickets--操作,直到tickets减到1停止,在执行到第①步的时候被切走了,而一号线程回来了,继续从它被打断的地方继续向后执行,也就是从第②步开始继续向后执行,在写回内存后,tickets已经减到了1,但是这个线程又把tickets修改为了999,并且这时它的时间片很长,所以这次又一直将tickets减到了1,由于判断条件tickets不为0,所以tickets继续减减操作,此时tickets减为了0,此时二号线程来了,将0读入到寄存器中进行减减操作,所以结果出现了-1,这就导致了问题的出现。

 要解决上面的问题,就需要做到以下三点:

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

要做到以上三点,就需要一把互斥锁,将临界区资源锁住,没有拿到钥匙的线程就不能访问临界区资源,这就能做到保护了临界区资源。Linux上提供的这把互斥锁叫互斥量。

💪互斥量的接口

初始化互斥量

有两种方式初始化互斥量

方法一:全局初始化分配

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER

方法二:局部初始化分配

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

参数:

  • mutex:要初始化的互斥量
  • attr:nullptr

返回值:成功返回0,失败返回错误码

销毁互斥量

 int pthread_mutex_destroy(pthread_mutex_t *mutex);

参数:

  • mutex:要销毁的互斥量

返回值:成功返回0,失败返回错误码

 销毁互斥量时需要注意

  • 使用全局初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量要确保后面的代码中不再有加锁的操作

互斥量的加锁与解锁

int pthread_mutex_lock(pthread_mutex_t *mutex);
int pthread_mutex_unlock(pthread_mutex_t *mutex);

返回值:成功返回0,失败返回错误码

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

  • 互斥量还没被加锁,处于未锁定状态,那么调用该函数会将互斥量加锁锁定。
  • 在调用该函数之前,其它线程已经申请了锁,锁定了该互斥量,或者存在其它线程同时竞争式的申请互斥量,但没有竞争到互斥量,那么调用pthread_mutex_lock就会被阻塞,等待会吃两解锁。

所以将上面的抢票代码修改如下:

int tickets = 1000; // 临界资源

class ThreadData
{
public:
    ThreadData(string &name, pthread_mutex_t *pmtx) : _tname(name), _pmtx(pmtx)
    {
    }

public:
    string _tname;
    pthread_mutex_t *_pmtx;
};


void *getTickets(void *args)
{
    ThreadData* td = (ThreadData*)args;
    while (true)
    {
        int n = pthread_mutex_lock(td->_pmtx); // 加锁保护临界区资源
        assert(n == 0);
        if (tickets > 0)
        {
            usleep(1000);
            printf("%s : %d\n", td->_tname.c_str(), tickets);
            cout << td->_tname << " : " << tickets << endl;
            tickets--;
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
        }
        else
        {
            n = pthread_mutex_unlock(td->_pmtx);
            assert(n == 0);
            break;
        }
        // 处理后续的动作
        cout << "恭喜,抢票成功" << endl;
        usleep(1000);
    }

    return nullptr;
}

#define THREAD_NUM 5

int main()
{
    pthread_mutex_t mtx;
    pthread_mutex_init(&mtx, nullptr); // 局部定义的锁进行初始化的形式
    pthread_t tid[THREAD_NUM];
    for (int i = 0; i < THREAD_NUM; i++)
    {
        string name = "thread ";
        name += to_string(i + 1);
        ThreadData *td = new ThreadData(name, &mtx);
        pthread_create(tid + i, nullptr, getTickets, (void *)td);
    }

    for (int i = 0; i < THREAD_NUM; i++)
    {
        pthread_join(tid[i], nullptr);
    }

    pthread_mutex_destroy(&mtx); // 最后将锁释放掉
    return 0;
}

 结果演示:

🔥探究互斥量实现原理

加锁的目的是保证操作的原子性。 从汇编的角度来看,如果只有一条汇编语句,我们就认为该汇编语句的执行是原子的, 在汇编中给我们提供了swap或者exchange指令,该指令的作用是将内存中的数据与CPU内寄存器中的数据(CPU内寄存器中的数据也叫做执行流的上下文,寄存器的空间是被所有执行流锁共享的,但是里面的数据是被某一个执行流私有的)进行交换,由于只有一条指令,所以可以保证其原子性。

解锁时会把互斥量变为1。

可重入函数和线程安全 

两者的概念区分 

线程安全:多个线程并发执行同一段代码时,不会出现不同的结果。
重入:同一个函数被不同的执行流调用,当前一个执行流还没有执行完,就有其它的执行流再次进入该函数,我们称这种情况是重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则称为不可重入函数。

常见的线程不安全和安全情况

不安全情况:

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

 安全情况:

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

可重入与线程安全的联系与区别

联系:

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

区别:

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

☀死锁 

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

产生死锁的四个必要条件

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

避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配
  • 对死锁检测
  • 银行家算法

🔥线程同步 

条件变量 

当我们申请临界资源前,要先检测临界资源是否存在,做检测的本质也是在访问临界资源,所以对临界资源的检测一定是要在加锁和解锁之间的。例如一个线程访问队列时,发现队列为空,那么它只能等待,直到其它线程将一个节点添加到队列中,在检测队列是否为空时,如果该线程一直轮询检测,那么势必要频繁的申请锁和释放锁,这样太浪费资源了,那么这种情况就需要用到条件变量了。

因此条件变量可以让线程不在频繁的自己检测了,当第一次检测到条件不满足时就挂起等待,当条件满足时,再通知该线程,让它来申请资源和访问。

同步的概念与竞态条件

同步:在保证数据安全的前提下,让线程能够按照某种特定的顺序访问临界资源,从而有效的解决了访问临界资源的合理性问题。

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

🔥条件变量接口

初始化 

和互斥量那里一样分为全局初始化和局部初始化

局部初始化 

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

参数

  • cond:要初始化的条件变量
  • attr:设置为nullptr即可

返回值:成功返回0,失败返回错误码

全局初始化

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);
参数

  • cond:要在这个条件变量上等待
  • mutex:互斥量

唤醒等待

int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒一批线程
int pthread_cond_signal(pthread_cond_t *cond);//唤醒某个线程

示例代码

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

#define NUM 4
typedef void (*func_t)(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond);
volatile bool quit = false;

class ThreadData
{
public:
    ThreadData(string &name, func_t func, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
    : _name(name), _func(func), _pmtx(pmtx), _pcond(pcond)
    {
    }

public:
    string _name;
    func_t _func;
    pthread_mutex_t *_pmtx;
    pthread_cond_t *_pcond;
};

void func1(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
    while(!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);//线程等待
        cout << name << " running... -- 1" << endl;
        // sleep(1);
        pthread_mutex_unlock(pmtx);
    }
}

void func2(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
    while(!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);//线程等待
        cout << name << " running... -- 2" << endl;
        // sleep(1);
        pthread_mutex_unlock(pmtx);
    }
}

void func3(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
    while(!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);//线程等待
        cout << name << " running... -- 3" << endl;
        // sleep(1);
        pthread_mutex_unlock(pmtx);
    }
}

void func4(const string name, pthread_mutex_t *pmtx, pthread_cond_t *pcond)
{
    while(!quit)
    {
        pthread_mutex_lock(pmtx);
        pthread_cond_wait(pcond, pmtx);//线程等待
        cout << name << " running... -- 4" << endl;
        // sleep(1);
        pthread_mutex_unlock(pmtx);
    }
}


void* Entry(void* args)
{
    ThreadData* tmp = (ThreadData*)args;
    tmp->_func(tmp->_name, tmp->_pmtx, tmp->_pcond);
    delete tmp;
    return nullptr;
}

int main()
{
    pthread_mutex_t mtx;
    pthread_cond_t cond;
    pthread_mutex_init(&mtx, nullptr);
    pthread_cond_init(&cond, nullptr);

    pthread_t tid[NUM];
    func_t funcs[NUM] = {func1, func2, func3, func4};
    for (int i = 0; i < NUM; i++)
    {
        string name = "thread ";
        name += to_string(i + 1);
        ThreadData* td = new ThreadData(name, funcs[i], &mtx, &cond);
        pthread_create(tid + i, nullptr, Entry, (void*)td);
    }

    int cnt = 10;
    while(cnt)
    {
        cout << "resume thread run code..." << cnt-- << endl;
        pthread_cond_signal(&cond);
        // pthread_cond_broadcast(&cond);
        sleep(1);
    }

    cout << "ctrl done" << endl;
    quit = true;
    pthread_cond_broadcast(&cond);

    for(int i = 0; i < NUM; i++)
    {
        pthread_join(tid[i], nullptr);
        cout << "pthread: " << tid[i] << " quit" << endl; 
    }

    pthread_mutex_destroy(&mtx);
    pthread_cond_destroy(&cond);
    return 0;
}

结果演示

 

按照一定的顺序执行。

🔥解释pthread_cond_wait中的互斥量

条件等待是线程间同步的一种手段,如果只有一个线程,条件不满足,一直等下去也都不会满足,所以必须还要有一个线程通过某些操作来改变共享变量,使得不满足的条件变得满足,并且友好的通知在条件变量上等待的线程。但是条件不会无缘无故的满足,这必然会牵扯到共享数据的改变。共享数据属于临界资源,因此一定要用互斥锁来保护,没有互斥锁的保护就无法安全的获取和修改共享数据了。

按照上面的说法,我们转换成代码,必须先上锁,检测到条件不满足时,pthread_cond_wait会解锁,然后在条件变量上等待,直到条件满足时,pthread_cond_wait又会重新加锁。

进入pthread_cond_wait函数后,会去检测条件是否满足,如果不满足就把互斥量变为1(解锁),直到条件满足后(pthread_cond_wait返回)将互斥量恢复成原样。

条件变量的规范使用如下

//等待条件代码
pthread_mutex_lock(&mtx);
while(条件检测)
    pthread_cond_wait(&cond, &mtx);
//修改条件
pthread_mutex_unlock(&mtx);


//条件满足,唤醒线程代码
pthread_mutex_lock(&mtx);
//设置条件满足
pthread_cond_signal(&cond);
pthread_mutex_unlock(&mtx);

 

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

go语言实战--基于Vue3+gin框架的实战Cetide网项目(讲解开发过程中的各种踩坑)

最近被要求学习go语言开发&#xff0c;也就做一个项目实战巩固一下&#xff0c;也分享一下关于gin框架的实战项目 &#xff08;后续应该还是会继续学习Java&#xff0c;这一期还是做一个govue的&#xff09; 经过一段时间的开发过后&#xff0c;感觉现在的开发效率要快不少了&…

你可以直接和数据库对话了!DB-GPT 用LLM定义数据库下一代交互方式,数据库领域的GPT、开启数据3.0 时代

✨点击这里✨&#xff1a;&#x1f680;原文链接&#xff1a;&#xff08;更好排版、视频播放、社群交流、最新AI开源项目、AI工具分享都在这个公众号&#xff01;&#xff09; 你可以直接和数据库对话了&#xff01;DB-GPT 用LLM定义数据库下一代交互方式&#xff0c;数据库领…

如何理解与学习数学分析——第二部分——数学分析中的基本概念——第6章——级数

第2 部分&#xff1a;数学分析中的基本概念 (Concepts in Analysis) 6. 级数(Series) 本章从等比级数(geometric series)开始&#xff0c;研究可以使用公式计算无限和的条件。它讨论了部分和与级数收敛的符号、图形表示和定义&#xff0c;并将它们应用于调和级数。它介绍了级…

EKF在LiFePO4电池SOC估算中不好用?一问带你破解EKF应用难题

磷酸铁锂电池因为平台区的存在&#xff0c;导致使用戴维南模型EKF的方法时&#xff0c;无法准确进行SOC准确预估。所以最近搜索了大量关于磷酸铁锂电池SOC预估的论文、期刊&#xff0c;但我被海量忽略客观事实、仅为了毕业的硕士论文给震惊到了。很多论文为了掩饰平台区的存在&…

Live800:深度解析,客户服务如何塑造品牌形象

在当今竞争激烈的市场环境中&#xff0c;品牌形象对于企业的成功至关重要。而客户服务作为品牌与消费者之间最直接的互动方式&#xff0c;不仅影响着消费者的购买决策&#xff0c;更在塑造品牌形象方面发挥着不可替代的作用。本文将深度解析客户服务如何塑造品牌形象&#xff0…

python文件:py,ipynb, pyi, pyc, pyd, pyo都是什么文件?

1、Python文件类型介绍 &#x1f4c1; 1.1 .py 文件&#xff1a;源代码基础 .py 文件是 Python 最基本的源代码文件格式&#xff0c;用于存储纯文本形式的 Python 代码。它是开发者编写程序的主要场所&#xff0c;包含函数、类、变量定义以及执行逻辑。Python 解释器直接读取…

C++ OpenCV 图像分类魔法:探索神奇的模型与代码

⭐️我叫忆_恒心&#xff0c;一名喜欢书写博客的研究生&#x1f468;‍&#x1f393;。 如果觉得本文能帮到您&#xff0c;麻烦点个赞&#x1f44d;呗&#xff01; 近期会不断在专栏里进行更新讲解博客~~~ 有什么问题的小伙伴 欢迎留言提问欧&#xff0c;喜欢的小伙伴给个三连支…

React hooks动态配置侧边栏

React hooks根据不同需求 还有不同的角色 动态的去配置侧边栏 需求&#xff1a; 点击某个按钮是一套侧边栏 &#xff0c;不同角色&#xff08;比如管理员之类的权限高一点&#xff09;比普通用户多个侧边栏 然后点击另一个按钮是另一套侧边栏 此时&#xff0c;就需要动态的去…

解决微信小程序分享按钮不可用

问题描述 在微信小程序中点击胶囊按钮上的三个点&#xff0c;在弹出的对话框中的【分享给好友】【分享到朋友圈】按钮都属于不可用的状态&#xff0c;显示未设置。 问题截图 解决方案 在每个需要此功能的页面都需要添加此代码&#xff0c;否则就不能进行使用。 // vue3时&l…

基础乐理入门

基础概念 乐音&#xff1a;音高&#xff08;频率&#xff09;固定&#xff0c;振动规则的音。钢琴等乐器发出的是乐音&#xff0c;听起来悦耳、柔和。噪音&#xff1a;振动不规则&#xff0c;音高也不明显的音。风声、雨声、机器轰鸣声是噪音&#xff0c;大多数打击乐器&#…

在UI界面中实现3d人物展示

简要原理(设置双摄像机): 为需要展示的3D人物单独设置一个摄像机(只设置为渲染人物层级),主要摄像机的方向与人物方向一致,但摄像机需要需要旋转180,设置的角度自行进行微调创建一个Render Texture类型的组件用于存储摄像机渲染的内容UI上设置需要展示的图片区域,图片…

台湾合泰原装BS66F360 封装LQFP-44 电容触摸按键 AD+LED增强型触控

BS66F360是一款由Holtek Semiconductor Inc.生产的微控制器&#xff08;microcontroller&#xff09;&#xff0c;具有触摸检测和LED驱动功能。其应用领域广泛&#xff0c;包括但不限于以下几个方面&#xff1a; 1. 触摸按键应用&#xff1a;BS66F360内置了触摸按键检测功能&am…

【MySQL】聊聊MySQL常见的SQL语句阻塞场景

在平时的业务中&#xff0c;可能一个简单的SQL语句也执行很慢&#xff0c;这种情况其实大多数都是要么没有使用索引&#xff0c;要么出现锁竞争造成执行阻塞。本篇主要来介绍具体的场景 CREATE TABLE t ( id int(11) NOT NULL, c int(11) DEFAULT NULL, PRIMARY KEY (id) ) ENG…

17.调用游戏本身的hp减伤害函数实现秒杀游戏角色

上一个内容&#xff1a;16.在目标进程构建CALL执行代码 16.在目标进程构建CALL执行代码在它的代码上进行的更改&#xff0c;它的callData变量中的代码不完善一个完整的函数是有return的&#xff0c;处理器执行到return会返回如果执行不到会继续往下走&#xff0c;直到执行不下…

像素着色技术在AI绘画中的革新作用

摘要&#xff1a;随着人工智能技术的不断进步&#xff0c;AI绘画已成为艺术和技术领域中的一个热门话题。本文将探讨像素着色技术在AI绘画中的应用及其对创作过程的影响&#xff0c;揭示这一技术如何推动艺术创作的革新。 引言&#xff1a; 传统的绘画方法要求艺术家具备高超的…

Nextjs学习教程

一.手动创建项目 建议看这个中文网站文档,这个里面的案例配置都是手动的,也可以往下看我这个博客一步步操作 1.在目录下执行下面命令,初始化package.json文件 npm init -y2.安装react相关包以及next包 yarn add next react react-dom // 或者 npm install --save next react…

kafka的leader和follower

leader和follower kafka的leader和follower是相对于分区有意义的&#xff0c;不是相对于broker。 因为每个分区都有leader和follower, leader负责读写数据。 follower负责复制leader的数据保存到自己的日志数据中&#xff0c;并在leader挂掉后重新选举出leader。 kafka会再…

【Unity】 HTFramework框架(五十一)代码片段执行器

更新日期&#xff1a;2024年6月8日。 Github源码&#xff1a;[点我获取源码] Gitee源码&#xff1a;[点我获取源码] 索引 Code Snippet Executer 代码片段执行器使用 Code Snippet Executer打开 Code Snippet Executer动态执行&#xff08;代码片段&#xff09;静态执行&#x…

从 Android 恢复已删除的备份录

本文介绍了几种在 Android 上恢复丢失和删除的短信的方法。这些方法都不能保证一定成功&#xff0c;但您可能能够恢复一些短信或其中存储的文件。 首先要尝试什么 首先&#xff0c;尝试保留数据。如果你刚刚删除了信息&#xff0c;请立即将手机置于飞行模式&#xff0c;方法是…

CSAPP Lab02——Bomb Lab完成思路详解

看见的看不见的 瞬间的永恒的 青草长啊大雪飘扬 ——月亮之上 完整代码见&#xff1a;CSAPP/bomb at main SnowLegend-star/CSAPP (github.com) 01 字符串比较 简单的把输入的字符串和地址“0x402400”内早已存储的字符串相比较。如果两个字符串相等则函数返回&#xff0c;否…