linux线程的同步与互斥

news2024/12/23 2:20:53

前面我们讲了线程的概念以及如何创建与控制线程,接下来我们来对线程的细节与线程之间的问题进行一些讲解;

1.线程的互斥

互斥就是相互排斥,我们可以理解为对立竞争不相容;线程的互斥则是线程之间在对于临界资源竞争时相互排斥的情况,通过这种情况可以处理一些对于临界资源访问所出现的问题,互斥是一种解决问题的方式

既然互斥是一种解决方案,那么它是用来解决什么样的问题的方案呢?

1.1需要互斥的场景

我们通过带入出现问题的场景来理解互斥:

假设我们设计了一个抢票系统,这个系统中有很多个线程,这些线程并发的进行抢票,直到票被抢完时停止抢票;

下面是代码示例:

#include <iostream>
#include <vector>
#include <pthread.h>
#include <string>
#include <unistd.h>
#include <sys/types.h>
using namespace std;

int ticket = 50;
//pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;

struct threadData
{
    string threadName;
    threadData(int num)
    {
        threadName = "thread" + to_string(num);
    }
};

void *getTicket(void *args)
{
    threadData *data = (threadData *)args;
    while (true)
    {
        // pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(10);//增加抢票的过程使得现象明显
            printf("%s get ticket sucess! remain ticket: %d\n", data->threadName.c_str(), --ticket);
            // pthread_mutex_unlock(&lock);
        }
        else
        {
            printf("%s quit!\n", data->threadName.c_str());
            // pthread_mutex_unlock(&lock);
            break;
        }
        // usleep(10);//抢完票的后序工作
    }
    return (void *)0;
}

int main()
{
    vector<pthread_t> tids;
    for (int i = 0; i < 3; i++)
    {
        pthread_t tid;
        threadData *data = new threadData(i);
        pthread_create(&tid, nullptr, getTicket, data);
        tids.push_back(tid);
    }

    for (auto tid : tids)
    {
        void *retData;
        pthread_join(tid, &retData);
    }

    return 0;
}

上面的代码中,我们创建了三个线程,每个线程都执行同一个函数进行抢票,当票数为0时就退出抢票循环;

现象:

 

这个就是问题所出现的场景,为什么会出现这样的错误呢?

其实早就在我们前面的进程间通信篇的最后部分就谈论过这样的问题;这是非原子性行为访问临界资源时产生的错误;由于我们抢票时的操作不是原子的,--ticket操作其实是由多条汇编指令所组成的;在线程运行到票数为1的时候,线程上下文不断切换,使得线程0,1,2都进入了循环中,此时票数只有最后一张,而三个线程都进入循环进行--,使得ticket的数量变为了0,-1,-2;

可以通过我之前画的这张图理解:

所以如果想要解决这样的问题就得在某个线程抢票时不会被其他线程所影响,这个时候互斥的方法就闪亮登场了;

1.2通过互斥的方式解决问题

如何使用互斥解决问题呢?我们首先得直到互斥的原理;

其实互斥就是通过给临界资源加锁来使得临界资源一次只能被一个线程所访问,从而使得让线程运行环境变安全;(用时间换安全)

第一步

所以我们想要通过互斥解决上面的问题,我们首先需要创建互斥量;

1.2.1互斥量

这个锁我们也可以称它为互斥量,互斥量是pthread原生线程库中的数据结构;我们只需要使用接口就可以创建;

互斥量的类型是pthread_mutex_t

我们可以通过下面两种方式创建:

1.静态方式创建:

创建全局或者静态变量:

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER

PTHREAD_MUTEX_INITIALIZER是一个宏变量,这个变量是专门用来初始化这样的全局mutex的;

2.动态方式创建

首先声明变量:

pthread_mutex_t mutex;

再初始化变量:

第一个参数mutex为需要初始化的mutex互斥量,传入前面声明的数据的地址即可;

第二个参数attr,是用来传一个设置互斥量属性的结构体的指针的,我们一般使用默认的,一般设置为NULL即可;

最后进行使用完后销毁即可:

参数与上面函数第一个参数相同,传mutex地址即可; 

第二步

创建好了mutex后,我们线程在访问临界资源时申请锁,申请成功后,其他线程申请锁时申请失败阻塞在lock代码处等待锁申请,这样也就自然不会出现多个线程同时访问同一临界资源的情况,当申请锁成功的线程访问完成临界资源后unlock解锁,允许其他线程申请锁,其他线程申请成功,解除阻塞;

互斥量的lock与unlock 

申请锁与释放锁的函数;

其实互斥量就是一个结构体的数据结构

1.3互斥的代码实现

    pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;     

    while (true)
    {
        pthread_mutex_lock(&lock);
        if (ticket > 0)
        {
            usleep(10);//增加抢票的过程使得现象明显
            printf("%s get ticket sucess! remain ticket: %d\n", data->threadName.c_str(), --ticket);
            pthread_mutex_unlock(&lock);
        }
        else
        {
            printf("%s quit!\n", data->threadName.c_str());
            pthread_mutex_unlock(&lock);
            break;
        }
        usleep(10);//抢完票的后序工作
    }

将第一份代码的注释代码取消注释后;我们看到我们在ticket判断和ticket--的地方都使用了lock与unlock包含;

现象:

 1.4互斥特点的总结

1.互斥是一种解决由于线程并发访问临界资源时锁产生问题的一种解决方式

2.互斥是一种用时间换安全的方式

3.互斥使得临界资源的使用代码串行执行

4.lock与unlock之间的代码越少越好,因为它们之间的代码是串行执行的,这里的太多会影响线程的效率

5.线程在执行lock与unlock之间的代码时是可以进行上下文切换的,因为线程线程的切换是持有锁切换的

1.5互斥原理总结

互斥是通过创建一个互斥锁,锁住我们的临界区代码,当线程执行临界区代码之前,需要先申请锁,锁申请成功时才可以访问临界区代码,而其他线程在这个时候是无法访问临界区的,因为此时的锁被占用了,其他线程会阻塞在lock处,当线程直线完了临界区代码时,unlock释放锁,让其他线程获取锁,从而执行它的临界区代码;这样的操作使得,线程访问临界区代码的动作在其他线程看来是原子的;使得临界资源的访问变得安全;

lock与ulock的底层:

lock底层:

先将0写入到寄存器al中,再将互斥量与al的值进行交换(互斥量一开始是需要被初始化,初始化为1)再进行判断al寄存器中的值,进行阻塞和申请成功的操作;

这个过程中mutex中的1只有一个,多个线程进行lock操作时,都会与al中的数据进行交换,al的数据一开始都是0,只有第一个执行xchgb的线程可以成功的将mutex中的1与寄存器中的0进行交换,使得al中的数据为1,从而满足申请条件,此时无论如何进行上下文交换,都无法让其他线程满足申请条件,因为唯一的mutex被交换到了某个线程的上下文数据的寄存器中;也正是有了这样的操作使得lock函数在线程角度是原子的;

ulock底层:

unlock操作就是只需要将mutex重新置为1即可让正在阻塞中的线程可以获取到这个1从而使得满足申请锁的条件;

1.6对锁的封装

C++中RAII风格的锁:

代码实现:

头文件
class Mutex
{
private:
    pthread_mutex_t _mutex;

public:
    Mutex()
    {
        pthread_mutex_init(&_mutex, nullptr);
    }
    ~Mutex()
    {
        pthread_mutex_destroy(&_mutex);
    }
    void lock()
    {
        pthread_mutex_lock(&_mutex);
    }
    void unlock()
    {
        pthread_mutex_unlock(&_mutex);
    }
};

class lockGuard
{
public:
    lockGuard(Mutex *mutex)
        : _mutex(mutex)
    {
        _mutex->lock();
    }
    ~lockGuard()
    {
        _mutex->unlock();
    }

private:
    Mutex *_mutex;
};
.c文件
struct threadData
{
    string _threadName;
    Mutex *_mutex;
    threadData(int num, Mutex *mutex)
        : _threadName("thread" + to_string(num)), _mutex(mutex)
    {
    }
    threadData() = default;
};

void *routine(void *args)
{
    threadData *data = static_cast<threadData *>(args);
    {
        lockGuard lock(data->_mutex);
        for (int i = 0; i < 5; i++)
        {
            printf("I am %s\n", data->_threadName.c_str());
        }
        sleep(1);
    }
}

int main()
{
    pthread_t tid;
    Mutex mutex;

    for(int i=0;i<3;i++)
    {
        threadData *data = new threadData(i, &mutex);
        pthread_create(&tid, nullptr, routine, data);
    }

    void *retData;
    pthread_join(tid, &retData);

    return 0;
}

这里我将init函数也封装进去了,这样可以Mutex类的创建也不需要手动的init和destroy了;

并且通过{}来限制了blockguard的生命周期:

现象:

2.死锁 

这种情况,其实就是线程1申请的锁是线程2所持有的,而线程2申请的锁是线程1所持有的,它们两个锁互相持有对方的资源,但是又不释放自己的资源导致的,程序一直处于阻塞状态的情况,这就叫做死锁;

一个线程死锁的情况:一次进行了两次lock;

多个线程死锁:

3.互斥场景引发的问题——饥饿

 我们将互斥代码实现的最后部分的usleep(10)注释掉

我们会看到这样一个现象:

而0,1线程完全阻塞在了lock位置没有任何的操作;这就是由于某个线程0在unlock锁后在上下文交换之前又马上回到循环开头申请锁,其他线程的竞争不过线程0,从而使得抢票过程只由线程0单独完成的情况;

由于某个线程的竞争能力太强而导致其他线程饥饿的问题;

为了解决这种情况产生的问题;又引出了新的解决方案——同步;

4.线程的同步 

同步也是一个为了解决互斥问题不足的解决方案;

4.1同步的过程(概念)

同步是如何做到解决饥饿问题的呢?同步通过让线程申请锁具有一定的顺序从而保证了所有线程可以依此访问临界资源,从而解决饥饿问题;那么如何让线程的申请变得有序呢?我们之间看下面的图片辅助理解吧:

什么是条件变量呢?在代码中的体现就是一个pthread_cond_t类型的变量我们可以叫他cond,这个变量也是一个结构体,这个条件变量可以辅助线程有序的申请临界资源;其实cond条件变量和我们的互斥量mutex(锁)的使用是非常相似的;

4.2条件变量

声明条件变量:

pthread_cond_t cond;

初始化与销毁:

它初始化的调用与mutex的十分相似,这里就不做过多讲解了;

将线程放入等待队列中:

pthread_cond_wait函数

这个函数可以将线程放入等待队列中等待,第一个参数是cond变量的地址,第二个参数是锁的地址;

唤醒函数:

pthread_cond_signal与pthread_cond_broadcast;

上面先通过图片示例讲解了同步的过程,接下来我们用代码的方式实现以下同步:

4.3同步的代码实现 

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

pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond=PTHREAD_COND_INITIALIZER;

struct threadData
{
    string _threadName;
    threadData(int num)
        : _threadName("thread" + to_string(num))
    {
    }
    threadData() = default;
};

void *routine(void *args)
{
    threadData *data = static_cast<threadData *>(args);
    {

        for (int i = 0; i < 5; i++)
        {
            pthread_mutex_lock(&mutex);
            pthread_cond_wait(&cond,&mutex);
            printf("I am %s\n", data->_threadName.c_str());
            pthread_mutex_unlock(&mutex);
            pthread_cond_signal(&cond);
        }
    }
}

int main()
{
    vector<pthread_t> tids;
    for(int i=0;i<3;i++)
    {
        pthread_t tid;
        threadData *data = new threadData(i);
        pthread_create(&tid, nullptr, routine, data);
        tids.push_back(tid);
    }
    sleep(1);
    pthread_cond_signal(&cond);
    void *retData;
    for(auto it:tids)
    {
        void *retData;
        pthread_join(it,&retData);
    }

    return 0;
}

上面的代码,我们可以看到我们让3个线程运行同一个函数,都进行打印自己线程的名字,我们通过同步后产生的现象:

 如果我们不进行上面的同步,将同步的代码注释掉:

 我们现在再来解释一下代码:

4.4同步实现的讲解

我们的代码做了什么呢;我们上面的代码中我们既有mutex又有cond,它们两个的存在帮助我们的代码实现了同步;首先在我们先对线程访问临界区代码(假设routine中的printf是访问临界资源的动作)上锁,之后我们的线程会先释放锁,然后再进入等待队列;

这一步做了什么呢呢?为什么我们的线程需要释放锁?为什么这一步需要再申请锁的后面?

1.因为我们线程进入进入等待队列时就会被阻塞住,如果此时不释放锁,线程携带锁进入阻塞队列一定会使得所有其他的线程无法获得锁也一直阻塞,从而导致死锁的发生,所以线程在进入等待队列前是会释放锁的;

2.为什么进入等待队列这个动作一定要在申请锁之后呢,因为临界区的进入一次只会有一个线程,而进入等待队列一般是因为临界区中的某个临界资源满足了进入等待队列的条件,从而线程判断出此条件后才会进入等待队列的,所以进入等待队列前一定会先访问临界资源,而为了保证安全访问临界资源的动作是一定要上锁后进行的,所以进入等待队列的动作一定要在申请锁之后这需要通过后面的生产消费模型讲解之后才能更好的理解),其次在上面那点中,我们进入等待队列前需要先释放锁,而如果代码顺序改变,我们都没有申请锁,又怎么可以释放锁呢,所以这也是代码顺序是如此的原因;

知道了上面这些之后,我们再来理一理上面代码的思路,首先多个线程都需要访问临界资源,为了保证安全临界资源被上了锁(临界区代码使用lock和unlock包住),其中一个线程成功申请到了锁,这个线程之间就开始执行wait代码了,也就是说它马上释放了锁,进入了等待队列中,等待被唤醒,而其他的线程原本在lock中阻塞突发发现锁空闲了,又马上竞争锁,之后又有一个线程成功竞争到了锁,然后再进入等待队列排在前面那个线程的后面,就这样依此类推所有的线程都进入了等待队列;我们再主线程中等待了1秒(sleep(1))让所有线程进入等待队列,之后再唤醒其中一个线程;当队头的线程被唤醒时,它在进入等待队列前先释放了锁,所以现在它退出等待队列也要再重新获得锁(如果锁被占用它会等待锁空闲再退出),然后再执行printf函数行为,所有动作执行完后,唤醒下一个线程,并释放锁;这就是上面代码的完整思路;

 接下来我们将上面同步的场景增强一些,我们来介绍一个新的模型:

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

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

相关文章

Unity | Shader基础知识(番外:了解内置Shader-Standard<一>)

前言 有粉丝给我说&#xff0c;感觉自己内部自带的Shader都还不知道怎么用&#xff0c;希望我讲一下内置Shader。 那我们就从Standard开始吧&#xff01;~ 一、什么是Standard standard是标准着色器&#xff0c;但标准着色器是什么意思&#xff1f; 官方解释&#xff1a;U…

推荐系统三十六式学习笔记:原理篇.内容推荐06|超越标签的内容推荐系统

目录 为什么要做好内容推荐&#xff1f;内容源内容分析和用户分析内容推荐算法总结&#xff1a; 基于内容的推荐系统&#xff0c;有个误区&#xff0c;衡量其性能优劣&#xff0c;评判标准是标签数量够不够。其实标签只是很小一部分。而且即便是标签&#xff0c;衡量质量的方式…

现在有一个生产计划,甲乙丙3个品类共16个产品,生产时间6天,每天甲品类可以生产1张单,乙3张,丙1张,请用MySQL写出H列的效果

现在有一个生产计划&#xff0c;甲乙丙3个品类共16个产品&#xff0c;生产时间6天&#xff0c;每天甲品类可以生产1张单&#xff0c;乙3张&#xff0c;丙1张&#xff0c;请用MySQL写出H列的效果吗&#xff1f; 最终展示结果要求为&#xff1a; 品类产品生产时间开始生产时间…

Nvidia 目前的市值为 3.01 万亿美元,超过苹果Apple

人工智能的繁荣将英伟达的市值推高到足以使其成为全球第二大最有价值的公司。 英伟达已成为全球第二大最有价值的公司。周三下午&#xff0c;这家芯片制造巨头的市值达到 3.01 万亿美元&#xff0c;领先于苹果公司的 3 万亿美元。 喜好儿网AIGC专区&#xff1a;https://heehe…

一文了解UVLED线光源的应用

在机器视觉系统中&#xff0c;光源作为不可或缺的一部分&#xff0c;能够提高目标成像效果&#xff0c;增强检测效果。光源的选择至关重要&#xff0c;选到不合适的会影响成像及检测效果。针对不同的检测对象,不同的形状光源应运而生。我们来看看最UVLED线光源。 下面以CCS的光…

Linux 36.3 + JetPack v6.0@jetson-inference之目标检测

Linux 36.3 JetPack v6.0jetson-inference之目标检测 1. 源由2. detectnet2.1 命令选项2.2 下载模型2.3 操作示例2.3.1 单张照片2.3.2 多张照片2.3.3 视频 3. 代码3.1 Python3.2 C 4. 参考资料 1. 源由 从应用角度来说&#xff0c;目标检测是计算机视觉里面第二个重要环节。之…

【python/pytorch】已解决ModuleNotFoundError: No module named ‘torch‘

【PyTorch】成功解决ModuleNotFoundError: No module named torch 一、引言 在深度学习领域&#xff0c;PyTorch作为一款强大的开源机器学习库&#xff0c;受到了众多研究者和开发者的青睐。然而&#xff0c;在安装和使用PyTorch的过程中&#xff0c;有时会遇到一些问题和挑战…

安全测试 之 安全漏洞: ClickHiJacking

1. ClickHiJacking 定义 点击劫持&#xff08;Click Jacking&#xff09;是一种视觉上的欺骗手段&#xff0c;攻击者通过使用一个透明的iframe&#xff0c;覆盖在一个网页上&#xff0c;然后诱使用户在该页面上进行操作&#xff0c;通过调整iframe页面的位置&#xff0c;可以使…

通用高电子迁移率晶体管(HEMT)的差分微变解算方案及分析型模型

来源&#xff1a;A Difference-Microvariation Solution and Analytical Model for Generic HEMTs&#xff08;TED 22年&#xff09; 摘要 这篇论文提出了一种AlGaN/GaN和AlGaAs/GaAs基高电子迁移率晶体管(HEMT)的分析型直流模型。该模型考虑了高栅偏压下势垒层中积累的电荷。…

Nat Commun|直捣黄龙!空间单细胞组学发现外周血靶细胞亚群的组织落脚点

溃疡性结肠炎&#xff08;Ulcerative colitis&#xff0c;UC&#xff09;是一种慢性炎症性肠道疾病&#xff0c;其特征是免疫介导的黏膜炎症和上皮损伤。目前UC的治疗策略已经从单纯的症状缓解转变为更精准的靶向治疗。Vedolizumab&#xff08;VDZ&#xff09;作为一种抗整合素…

短剧系统源码:打造多平台互动娱乐体验

随着科技的不断进步&#xff0c;多平台互动娱乐体验逐渐成为人们日常生活的一部分。短剧系统源码便是在这样的背景下应运而生&#xff0c;它为开发者提供了一套完整的解决方案&#xff0c;以实现PC端、移动端APP、H5网页以及小程序平台的无缝对接和内容共享。本文将详细介绍短剧…

在iPhone上恢复已删除的Safari历史记录的最佳方法

您是否正在寻找恢复 iPhone 上已删除的 Safari 历史记录的最佳方法&#xff1f;好吧&#xff0c;这篇文章提供了 4 种在有/无备份的情况下恢复 iPhone 上已删除的 Safari 历史记录的最佳方法。现在按照分步指南进行操作。 iPhone 上的 Safari 历史记录会被永久删除吗&#xff1…

开关管的导通损耗和开关损耗及MOS管驱动

导通损耗和开关损耗 开关管的损耗带来的问题 ★使得开关管发热&#xff0c;为了降低温度&#xff0c;需要增大散热面积&#xff0c;从而增大了体积和成本。 ★降低了开关电源的效率。 开关管的损耗分为导通损耗和开关损耗 ★导通损耗&#xff1a;在开关管导通时候&#xf…

重庆耶非凡科技揭秘:海外拼多多选品师岗位真相解析

在当今数字化、全球化的浪潮中&#xff0c;跨境电商行业蓬勃发展&#xff0c;越来越多的企业开始寻求拓展海外市场的新机遇。其中&#xff0c;重庆耶非凡科技有限公司作为一家在软件开发和技术服务领域具有深厚实力的企业&#xff0c;其海外拼多多选品师这一职位引发了广泛关注…

计算机网络学习记录 应用层 Day6

你好,我是Qiuner. 为记录自己编程学习过程和帮助别人少走弯路而写博客 这是我的 github https://github.com/Qiuner ⭐️ ​ gitee https://gitee.com/Qiuner &#x1f339; 如果本篇文章帮到了你 不妨点个赞吧~ 我会很高兴的 &#x1f604; (^ ~ ^) 想看更多 那就点个关注吧 我…

如何将AndroidStudio和IDEA的包名改为分层级目录

新版UIAndroidStudio 1、点击项目目录右上角如图所示的三个点点。 2、然后依次取消Hide empty middle package &#xff0c;Flatten package的勾选 3、注意&#xff1a;一定要先取消hide的勾选&#xff0c;不然目录不会完全分级&#xff08;做错了可以反过来重新设置&#x…

[leetcode hot 150]第七十题,爬楼梯(动态规划)

题目&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 爬到第 n 阶楼梯的方法数量等于爬到第 n-1 阶和第 n-2 阶的方法数量之和,即: f(n) f(n-1) f(n-2) 边界条件 还需要考虑边界…

ChatGPT Prompt技术全攻略-入门篇:AI提示工程基础

系列篇章&#x1f4a5; No.文章1ChatGPT Prompt技术全攻略-入门篇&#xff1a;AI提示工程基础2ChatGPT Prompt技术全攻略-进阶篇&#xff1a;深入Prompt工程技术3ChatGPT Prompt技术全攻略-高级篇&#xff1a;掌握高级Prompt工程技术4ChatGPT Prompt技术全攻略-应用篇&#xf…

独著出书的出版流程是怎样的?

独著出书的出版流程一般包括以下几个步骤&#xff1a; 1. 准备书稿&#xff1a;确保书稿内容完整、准确&#xff0c;并符合出版社的要求。 2. 选择出版社&#xff1a;根据书稿的主题和内容&#xff0c;选择合适的出版社。可以考虑出版社的专业性、声誉和出版范围等因素。 3.…

KT142C语音芯片下载语音过连接电脑PC的usb虚拟成U盘拷贝

一、简介 KT142C-sop16语音芯片&#xff0c;下载语音是连接电脑PC的usb虚拟成U盘直接拷贝 下载方式描述 内置的是320K的空间&#xff0c;但是实际的大小&#xff0c;在电脑里面显示&#xff0c;应该是315Kbyte。 打开我的电脑&#xff0c;芯片连接PC之后&#xff0c;自动多出…