linux线程的同步与互斥

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

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函数行为,所有动作执行完后,唤醒下一个线程,并释放锁;这就是上面代码的完整思路;

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

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

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

相关文章

openh264 编码命令行工具源码分析

openh264 OpenH264 是由 Cisco 公司发布的一个开源的 H.264 编码和解码器。它提供了命令行工具&#xff0c;可以用于对视频进行编码和解码操作。 使用说明 openh264 编码命令行工具可以使用命令行或 config 配置进行编码操作。编译和使用方法具体可以参考 Windows11编译open…

12_JavaWebAjax

文章目录 Ajax1. 同步请求异步请求2. Ajax实现方式3. 日程管理第四期4. 响应JSON串4.1 响应JSON串格式的一般格式 Appendix Ajax 发送请求的一些方式 1.输入浏览器回车 2.html>head>script/link ​ img标签 3.a标签form表单标签等 用户手动控制提交产生&#xff1b…

实验七、创建小型实验拓扑《计算机网络》

早检到底是谁发明出来的。 一、实验目的 完成本实验后&#xff0c;您将能够&#xff1a; • 设计逻辑网络。 • 配置物理实验拓扑。 • 配置 LAN 逻辑拓扑。 • 验证 LAN 连通性。 二、实验任务 在本实验中&#xff0c;将要求您连接网络设备并配置主机实现基本的网络…

R语言探索与分析20-北京市气温预测分析

一、序言 近年来&#xff0c;人类大量燃烧煤炭、天然气等含碳燃料导致温室气 体过度排放&#xff0c;大量温室气体强烈吸收地面辐射中的红外线&#xff0c;造 成温室效应不断累积&#xff0c;使得地球温度上升&#xff0c;造成全球气候变暖。气象温度的预测一直以来都是天气预…

计算机毕业设计 | 基于node(Koa)+vue 高校宿舍管理系统 宿舍可视化(附源码)

1&#xff0c;绪论 1.1 项目背景 随着科技的发展&#xff0c;智能化管理越来越重要。大学生在宿舍的时间超过了1/3&#xff0c;因此良好的宿舍管理对学生的生活和学习极为关键。学生宿舍管理系统能够合理安排新生分配宿舍&#xff0c;不浪费公共资源&#xff0c;减轻学校管理…

微信扫普通二维码后通过小程序观看的实现

为了方便小程序开发者更便捷地推广小程序&#xff0c;兼容线下已有的二维码&#xff0c;微信公众平台开放扫描普通链接二维码跳转小程序能力。 功能介绍 普通链接二维码&#xff0c;是指开发者使用工具对网页链接进行编码后生成的二维码。 线下商户可不需更换线下二维码&…

【面试干货】SQL中count(*)、count(1)和count(column)的区别与用法

【面试干货】SQL中count&#xff08;*&#xff09;、count&#xff08;1&#xff09;和count&#xff08;column&#xff09;的区别与用法 1、count(*)2、count(1)3、count(column) &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在SQL中&a…

基于GTX 8B10B编码的自定义PHY上板测试(高速收发器十四)

前文整理了GTX IP&#xff0c;完成了自定义PHY协议的收发模块设计&#xff0c;本文将通过光纤回环&#xff0c;对这些模块上板测试&#xff0c;首先需要编写一个用于生成测试数据的用户模块。 1、测试数据生成模块 本模块用于生成自定义PHY协议的测试数据&#xff0c;通过axi_…

【微信小程序】页面导航

声明式导航 导航到 tabbar 页 tabBar页面指的是被配置为tabBar的页面。 在使用<navigator>组件跳转到指定的tabBar页面时&#xff0c;需要指定url属性和open-type属性&#xff0c;其中&#xff1a; url 表示要跳转的页面的地址&#xff0c;必须以/开头open-type表示跳…

Python轻量级嵌入式关系数据库之apsw使用详解

概要 在现代应用开发中,数据库是一个非常重要的组成部分。SQLite 是一个轻量级的嵌入式关系数据库管理系统,被广泛应用于各种应用程序中。APSW(Another Python SQLite Wrapper)库是一个专门用于访问 SQLite 数据库的 Python 包,它提供了 SQLite 所有的功能,并且比标准库…

usb设备在主机和VMWare虚拟机中切换连接

操作&#xff1a;点击菜单栏虚拟机(M)>可移动设备>选择自己的usb设备>连接(断开与 主机 的连接)

【会议征稿,ACM出版】2024年图像处理、智能控制与计算机工程国际学术会议(IPICE 2024,7月9-11)

2024年图像处理、智能控制与计算机工程国际学术会议&#xff08;IPICE 2024&#xff09;将于2024年7月9-11日在中国福州举行。本届会议由阳光学院、福建省空间信息感知与智能处理重点实验室、空间数据挖掘与应用福建省高校工程研究中心联合主办。 会议主要围绕图像处理、智能控…

巨详细Linux安装Tomcat教程

巨详细Linux安装Tomcat教程 1、检查是否残留其他版本2、上传安装包至服务器2.1安装包获取2.2创建相关目录 3、安装Tomcat3.1安装3.2启动3.3web页面 4、配置Tomcat4.1把tomcat进程交给systemctl管理4.2设置tomcat开机自启动 1、检查是否残留其他版本 #检查残留数据 rpm -qa|gre…

Eclipse添加C和C++编译成汇编文件的选项

在miscellaneous中添加assemble listing选项就可以生成汇编文件了

C++标准模板(STL)- 迭代器库-迭代器适配器- 逆序遍历的迭代器适配器 (二)

迭代器库-迭代器原语 迭代器库提供了五种迭代器的定义&#xff0c;同时还提供了迭代器特征、适配器及相关的工具函数。 迭代器分类 迭代器共有五 (C17 前)六 (C17 起)种&#xff1a;遗留输入迭代器 (LegacyInputIterator) 、遗留输出迭代器 (LegacyOutputIterator) 、遗留向前…

网站安全小白也能搞定的SSL证书安装免费方法

大家都知道&#xff0c;部署一个网站&#xff0c;除了购买域名&#xff0c;现在基本标配SSL证书。 我们以aliyun为例 大家看到这个&#xff0c;收费的SSL证书几千-几万1年不等。这时候&#xff0c;你就会想有没有免费的可以搞。linux老鸟都知道&#xff0c; Let’s Encrypt 、…

微信小程序多端框架打包后发布到华为市场

app上架华为应用市场 一、android 发布到华为应用市场 1、华为应用市场注册开发者账号 https://developer.huawei.com/consumer/cn/?ha_sourcesem&ha_sourceId89000605 2、进行企业认证 3、app隐私弹窗 miniapp-privacy.json 1、协议弹窗内容&#xff1a; {"tit…

创新“智”领长江经济带高质量发展研讨会调研实在智能

近日&#xff0c;创新“智”领长江经济带高质量发展研讨会暨央企智库沙龙第46期在浙江杭州顺利召开。 会议由中国联通主办&#xff0c;中国联通研究院、浙江联通、浙江省人民政府国有资产监督管理委员会联合承办&#xff0c;长江经济带沿线中央企业、地方国资国企等60余家单位…

SwiftUI中ContentUnavailableView的使用(iOS 17、tvOS 17推出的新组件)

iOS 17为SwiftUI带来了一个新的组件ContentUnavailableView&#xff0c;它允许我们向用户呈现一个空状态&#xff0c;而不需要创建自定义错误或者无内容视图。 ContentUnavailableView易于使用&#xff0c;可自定义&#xff0c;并且具有用于空搜索状态的预定义视图。 建议在无…