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

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

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

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

相关文章

mathematica中针对三维图中的颜色和填充透明度进行指定

颜色指定使用的命令为&#xff1a;PlotStyle 填充的透明度使用的命令为&#xff1a;FillingStyle 示例代码&#xff1a; Clear["Global*"] Plot3D[{Sin[x^2 y], Sin[x^2 - y]}, {x, -2, 2}, {y, -2, 2}, PlotStyle -> {Directive[Red, Specularity[White, 100…

Java面试八股之组合、聚合和关联三者的区别是什么

组合、聚合和关联三者的区别是什么 关联&#xff08;Association&#xff09;: 最基本的一种关系&#xff0c;表示一个类知道另一个类的存在&#xff0c;或者说是类之间的某种联系。 关联可以是双向的也可以是单向的&#xff0c;且不规定参与关联的对象的生存周期。 实例&a…

使用命令给电脑添加虚拟网卡和IP

目录 1、添加网卡 1-1、windows系统添加网卡 1-2、Linux系统中添加网卡 2、添加IP和DNS 2-1、添加IP 2-2、 设置DNS 3、删除网卡 3-1、Windows: 3-2、Linux 3-3、macOS 4、示例&#xff1a; 首先以管理员方式进入CMD命令行&#xff1b; 点击“开始”->“管理员…

【数据分享】《中国文化文物与旅游统计年鉴》2022

最近老有同学过来询问《中国旅游年鉴》、《中国文化文物统计年鉴》、《中国文化和旅游统计年鉴》、《中国文化文物与旅游统计年鉴》&#xff0c;这四本年年鉴的关系以及怎么获取这四本年鉴。今天就在这里给大家分享一下这四本年鉴的具体情况。 实际上2018年&#xff0c;为适应…

java之面向对象

1 面向对象介绍 <span style"background-color:#f8f8f8"><span style"color:#333333">1.面向过程:自己的事情自己干,代表语言C语言洗衣服:每一步自己要亲力亲为 -> 找个盆,放点水,找个搓衣板,搓搓搓 2.面向对象:自己的事情别人帮忙去干,代…

什么是突发性耳聋?

72小时内突然发生、原因不明的感音神经性听力损失&#xff0c;至少在相邻的两个频率听力下降≥20dBHL。 特点&#xff1a; 1发生在数分钟、数小时或3天以内的听力下降&#xff1b; 2原因不明&#xff1b; 3多发生于单侧&#xff0c;可伴有耳鸣、耳堵塞感及耳周麻木感&#…

【ARM Cache 及 MMU 系列文章 6.1 -- Cache maintenance 相关寄存器及指令详细介绍】

请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】 及【嵌入式开发学习必备专栏】 文章目录 Cache Maintenance registers and instructionsDCZID_EL0DCZID_EL0寄存器字段解释 DCZ 使用场景Cache maintenance 范围选择 Cache maintenance 指令集 Cache Maintenance registers a…

C# E2Pose人体关键点检测(OpenVINO推理)

C# E2Pose人体关键点检测(OpenVINO推理) 目录 效果 模型信息 项目 代码 下载 效果 模型信息 Inputs ------------------------- name&#xff1a;inputimg tensor&#xff1a;Float[1, 3, 512, 512] --------------------------------------------------------------- Ou…

Python代码大使用Paramiko轻松判断文件类型,提取上级目录

哈喽&#xff0c;大家好&#xff0c;我是木头左&#xff01; 一、Paramiko简介 Paramiko是一个用于SSHv2协议的Python实现&#xff0c;提供了客户端和服务器功能。它可以用于远程连接和管理服务器&#xff0c;执行命令、上传下载文件等。本文将介绍如何使用Paramiko判断文件类…

数据挖掘分析的一点进步分享

import pandas as pd import matplotlib.pyplot as plt import numpy as npdata pd.read_csv(heros.csv,encoding"gbk") data.head() 导入数据集 进行分析 df_datadata.copy() df_data.describe()df_data.info() df_data.drop(英雄,axis1,inplaceTrue) df_data[最…

饥荒云服务器卡顿情况如何解决

随着网络游戏的普及&#xff0c;云服务器逐渐成为游戏开发者和玩家们的首选。然而&#xff0c;在使用饥荒云服务器时&#xff0c;有时会遇到卡顿的问题&#xff0c;这给玩家带来了困扰。本文将探讨饥荒云服务器卡顿的原因&#xff0c;并提供一些可能的解决方法。 卡顿产生的原因…

实践分享:如何用小程序里的小组件做应用开发?

随着移动互联网的快速发展&#xff0c;小程序等轻量级应用平台日益成为用户获取信息和服务的重要渠道。而小组件也在其中扮演了至关重要的角色&#xff0c;不仅能够提升用户的交互体验&#xff0c;还能帮助开发者高效地构建功能丰富、界面美观的小程序。 本文中&#xff0c;我们…

构建第一个ArkTS应用之@卡片使用动效能力

ArkTS卡片开放了使用动画效果的能力&#xff0c;支持显式动画、属性动画、组件内转场能力。需要注意的是&#xff0c;ArkTS卡片使用动画效果时具有以下限制&#xff1a; 表1 动效参数限制 名称 参数说明 限制描述 duration 动画播放时长 限制最长的动效播放时长为1秒&…

JavaWeb2-Vue

Vue 前端框架&#xff0c;免除原生JS中的DOM操作简化书写 &#xff08;以前学过又忘了&#xff0c;现在才知道原来vue是前端的&#xff09; 基于MVVM思想&#xff08;model-view -viewModel&#xff09;实现数据双向绑定 model是数据模型 view负责数据展示 即DOM 中间这个负责…

堆排序讲解

前言 在讲堆的删除时&#xff0c;我们发现一步一步删除堆顶的数据&#xff0c;排列起来呈现出排序的规律&#xff0c;所以本节小编将带领大家进一步理解堆排序。 1.堆排序概念 那么什么是堆排序&#xff1f; 堆排序&#xff08;Heap Sort&#xff09;是一种基于堆数据结构的排…

网络学了点socket,写个聊天室,还得改进

目录 第一版: common 服务端: 客户端 第一版问题总结: 第二版 服务端: 客户端: 改进: Windows客户端 一些小问题 还可以进行的改进 这篇文章我就先不讲网络基础的东西了,我讲讲在我进行制作我这个拉跨聊天室中遇到的问题,并写了三版代码. 第一版: common #pragm…

SpringBoot-集成TOTP

TOTP验证码提供了一种高效且安全的身份验证方法。它不仅减少了依赖短信或其他通信方式带来的成本和延时&#xff0c;还通过不断变换的密码增加了破解的难度。未来&#xff0c;随着技术的进步和对安全性要求的提高&#xff0c;TOTP及其衍生技术将继续发展并被更广泛地应用。TOTP…

多模态模型是什么意思(国内外的AI多模态有哪些)

在人工智能和机器学习的领域&#xff0c;我们经常会遇到一些专业术语&#xff0c;这些术语可能会让初学者感到困惑。其中&#xff0c;"多模态模型"就是这样一个概念。 什么是AI多模态。它是什么意思呢&#xff1f; 那么&#xff0c;多模态模型是什么意思呢&#xff1…

【Python】数据处理:SQLite操作

使用 Python 与 SQLite 进行交互非常方便。SQLite 是一个轻量级的关系数据库&#xff0c;Python 标准库中包含一个名为 sqlite3 的模块&#xff0c;可以直接使用。 import sqlite3数据库连接和管理 连接到 SQLite 数据库。如果数据库文件不存在&#xff0c;则创建一个新数据库…

SystemVerilog Interface Class的妙用

前言 Interface Class是在SystemVerilog 2012版本中引入的&#xff0c;但目前在验证中几乎很少采用&#xff0c;大多数验证工程师要么不知道它&#xff0c;要么没有看到使用它的任何好处&#xff0c;这使得Interface Class成为一个未被充分使用和不被重视的特性。本文将举两个…