Linux系统编程--线程同步

目录

一、前言

二、线程饥饿

三、线程同步

四、条件变量

1、cond

2、条件变量的使用

五、条件变量与互斥锁


一、前言

        上篇文章我们讲解了线程互斥的概念,为了防止多个线程同时访问一份临界资源而出问题,我们引入了线程互斥,线程互斥其实就是多个线程同时争抢一份资源,谁抢到了就是谁的,抢不到的只能等待着下一次抢。虽然解决了有多个线程同时访问同一资源所产生的问题,但是我们思考一下这样子合理吗?不合理,这会产生另一种问题——线程饥饿。

二、线程饥饿

       那么线程饥饿是什么呢?为了便于理解,我们可以极端地考虑问题,假设在多线程情况下,存在着两类优先级不同的线程,一类线程的优先级非常高,另一类的线程的优先级非常低,他们开始同时争抢临界资源,假设高优先级的线程拿到了资源,上了锁之后,其他的线程只能等。直到该线程使用完临近资源后解锁,接着所有线程又开始争抢资源,而高优先级的线程因为其优先性会再一次争抢到资源,如循环往复,导那些低优先级的线程总是在等待中,永远拿不到或者很少次数拿到资源,这样被称为饥饿或者饿死。这种争抢临界资源的方式虽然是没有什么错误,但是总归来说是不合理的。

三、线程同步

       在线程只使用互斥的方式去访问临界资源的时候,就可能会出现某些线程饥饿的情况。那么在操作系统中有没有一种机制,在某一时刻既可以只让一个线程去访问临界资源,但是又可以让所有的的线程按照一定的顺序访问资源呢?所有的线程就像排队一样一个个轮流访问资源,当某一线程访问玩临界资源的时候,他就去队尾等待。这样所有的线程的执行流都可以访问到资源,从而杜绝了线程饥饿的问题。  这样的机制叫做——同步,即线程同步,在保证临界资源安全的前提下,让执行流访问临界资源具有一定的顺序性

互斥也是同步的一种,尽管只采用互斥后执行流还是乱序的,但是互斥保证了同一时刻只能有一个线程访问临界资源。但是本篇文章在介绍同步的时候,会将两者分开,即同步不包括互斥。

四、条件变量

 那么同步是怎么实现的呢?同步离不开一个东西——条件变量条件变量是一种可以实现线程同步的机制,通过条件变量,可以实现让线程有序的访问临界资源

条件变量,顾名思义它是一个执行的“条件”,当线程需要访问临界资源时,如果临界资源不满足一定的条件,那就让线程进行等待,如果满足条件,则让线程继续恢复执行的机制。它是一个 pthread_cond_t 结构体类型的变量,并且在 pthread 库中也提供了一些条件变量相关的接口


1、cond

cond即 英文单词 condition 的缩写,译为条件。

pthread_cond_t 是定义条件变量的类型。

条件变量的使用是和互斥锁差不多的。

  • 条件变量的初始化可以和互斥量相同有两种,一种是调用接口 pthread_cond_init() 初始化,第一个参数是条件变量的地址,第二个参数是条件变量的属性(暂时不考虑)。需要注意的是,用该接口初始化的条件变量在不需要使用的时候,需要调用 pthread_cond_destroy() 接口来销毁掉。 
  • 使用宏初始化的条件变量就不用手动调用接口来销毁了。

使用条件变量等待的接口: 

  •  这么多等待的接口中 pthread_cond_wait() 接口是最常用的,它是pthread库提供的使用条件变量等待的接口,线程调用此接口,线程就会立即进入等待。
  • pthread_cond_timedwait() 也是pthread提供给的使用条件变量等待的接口,不过看他的名字也知道它是一种定时让线程等待的接口,即可以通过该接口设置一定的时间,在此时间内让线程等待,如果此时间内,条件满足了,线程就会被自动唤醒,继续执行代码
  • 我们可以看到这两个接口的参数中都有 互斥锁 ,他们是和互斥锁一起配合使用的。

上面讲到了两个通过条件变量让线程进行等待的接口,既然有等待的接口,那么自然就存在着通过条件变量去唤醒线程的接口。如下

  • pthread_cond_signal(),调用该接口可以让某个通过指定条件变量陷入等待的线程被唤醒。
  • pthread_cond_broadcast(),调用此接口,可以让通过指定条件变量陷入等待的所有线程被唤醒

2、条件变量的使用

下面我们简单使用一下条件变量,主要看看它是怎么用的。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;//利用宏初始化全局互斥锁,不用销毁
pthread_cond_t cond;//定义全局条件变量

void* Callback(void* argc)
{
    pthread_detach(pthread_self());//这里让线程自动分离,我们后面不回收它
    const char* name=(const char*)argc;
    while(true)
    {
        pthread_cond_wait(&cond,&mutex);//使用条件变量让进程在这里等待
        cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;
    }
    return nullptr;

}
int main()
{
    pthread_cond_init(&cond,nullptr);//初始化条件变量
    pthread_t tid1,tid2,tid3;

    pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");
    pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");
    pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");
    
    while(true)
    {
        char c='a';
        cout<<"Please input your command:(N/Q)::";
        cin>>c;
        if(c=='N'|c=='n')
        {
            pthread_cond_signal(&cond);//唤醒单个的线程
        }
        else
            break;
        usleep(1000);//让主线程在这里等待一下防止多线程之间的打印干扰
    }
        pthread_cond_destroy(&cond);//销毁条件变量
        return 0;
    }

运行结果:

可以看到pthread_cond_signal()对线程的唤醒是以一定顺序来进行的。当然我们也可以使用pthread_cond_broadcast()来广播唤醒所有的在等待中的线程。


上面演示的是cond变量的简单使用,我们在函数中直接让它进行等待,事实上在实际的使用中,当有条件变量不满足时,才会使用条件变量让线程等待。

我们可以设置一个退出条件 quit,为真时即为满足,否则不满足。不满足条件时,就让线程等待,满足条件就唤醒线程。

#include<iostream>
#include<pthread.h>
#include<unistd.h>
using std::cin;
using std::cout;
using std::cerr;
using std::endl;
pthread_mutex_t mutex=PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond;
volatile bool quit=false;
void* Callback(void* argc)
{
    pthread_detach(pthread_self());
    const char* name=(const char*)argc;
    while(!quit)
    {
        pthread_cond_wait(&cond,&mutex);
        cout<<name<<",tid::"<<pthread_self()<<",running"<<endl;
    }
//下面释放锁的操作是因为pthread_cond_wait()接口在等待时会释放锁资源,然后被唤醒的时候又会竞争锁资源,如果线程退出条件满足了,在退出的时候,仍然是对临界资源上了锁,所以在退出之前需要先解锁,不然会导致死锁(如果不提前进行分离)
    pthread_mutex_unlock(&mutex);
    cout<<name<<",tid::"<<pthread_self()<<",end"<<endl;
    return nullptr;

}
int main()
{
    pthread_cond_init(&cond,nullptr);
    pthread_t tid1,tid2,tid3;

    pthread_create(&tid1,nullptr,Callback,(void*)"thread 1");
    pthread_create(&tid2,nullptr,Callback,(void*)"thread 2");
    pthread_create(&tid3,nullptr,Callback,(void*)"thread 3");
    
    while(true)
    {
        char c='a';
        cout<<"Please input your command:(N/Q)::";
        cin>>c;
        if(c=='N'|c=='n')
        {
            pthread_cond_broadcast(&cond);
        }
        else{
            quit=true;
            pthread_cond_broadcast(&cond);
            break;
        }
        usleep(1000);
    }
        pthread_cond_destroy(&cond);
        return 0;
    }

这里比之前的简单应用主要多了一个解锁操作。且在 输入非N或n时,唤醒线程,再让线程判断一下条件是否满足。

可以看到 使用条件变量可以让多线程的执行具有一定的顺序性,即可以实现同步。同步与互斥是互补的关系。

五、条件变量与互斥锁

在我们上面所举的例子当中,让线程根据条件变量进行等待的接口都是需要同时用到条件变量和互斥锁,使用到条件变量这是无可厚非的,但是为什么需要用到互斥锁呢?

首先,条件等待是使用条件变量实现同步等待的一种方式,如果只存在一个线程的话,当条件不满足时,线程就会一直等待下去,因为唯一的线程在等待,并没有其他的线程修改条件,所以在线程等待的时候,条件也不可能满足。

所以这里需要的是一个使得条件变得满足,然后再唤醒等待的线程。这里的条件实际上就是指 线程对应的需要访问的临界资源的状态,就像我们在介绍互斥时的抢票动作,需要保证只有在票数大于0时,才能抢票。

而条件是不可能无缘无故在没有变化的情况下就自己满足的,所以条件满足势必会存在着临界资源数据的变化,所以需要用互斥锁来保护临界资源。

所以线程在判断条件满足之前需要先上锁,然后再判断条件是否满足,如果不满足则条件等待并解锁,接着让其他可以让条件满足的线程获取锁,条件满足之后,再唤醒刚才等待的线程并解锁。让刚被唤醒的线程再次取到锁,判断条件是否满足,满足就去执行,否则再次陷入等待。整个过程的重点就是谁需要访问临界资源就上锁,谁不需要就解锁,即保证在整个的过程当中临界资源始终是被保护着的。

整个的过程当中,除了第一次对临界资源上锁和最后一次对临界资源解锁,中间所有的上锁和解锁操作都是由pthread_cond_wait()操作完成的,在线程需要等待的时候调用pthread_cond_wait()解锁并等待,在线程被唤醒时,会自动再去竞争锁,解锁和上锁操作都是在pthread_cond_wait()内部进行的。这就是为什么我们在上面的例子中在多线程退出时,需要在条件满足时先释放锁,然后再让线程退出。

pthread_cond_wait()接口需要执行释放锁和竞争锁的操作,所以需要先看到锁这也是为什么该接口需要和互斥锁一起使用。

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

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

相关文章

【HarmonyOS Next】鸿蒙加固方案调研和分析

【HarmonyOS Next】鸿蒙加固方案调研和分析 一、前言 根据鸿蒙应用的上架流程&#xff0c;本地构建app文件后&#xff0c;上架到AGC平台&#xff0c;平台会进行解析。根据鸿蒙系统的特殊设置&#xff0c;仿照IOS的生态闭环方案。只能从AGC应用市场下载app进行安装。这样的流程…

# 深入理解RNN(一):循环神经网络的核心计算机制

深入理解RNN&#xff1a;循环神经网络的核心计算机制 RNN示意图 引言 在自然语言处理、时间序列预测、语音识别等涉及序列数据的领域&#xff0c;循环神经网络(RNN)一直扮演着核心角色。尽管近年来Transformer等架构逐渐成为主流&#xff0c;RNN的基本原理和思想依然对于理…

深度学习实战车道线检测

深度学习实战车道线检测 这里写目录标题 车道线原理整体架构设计核心原理步骤1. 特征提取&#xff08;骨干网络&#xff09;2. 特征融合3. 车道线表示与分类4. 损失函数5. 后处理 速度优势的来源 软件实现安装环境与文件说明实验测试 结束语 车道线原理 Lane - Detection是一种…

【redis】五种数据类型和编码方式

文章目录 五种数据类型编码方式stringhashlistsetzset查询内部编码 五种数据类型 字符串&#xff1a;Java 中的 String哈希&#xff1a;Java 中的 HashMap列表&#xff1a;Java 中的 List集合&#xff1a;Java 中的 Set有序集合&#xff1a;除了存 member 之外&#xff0c;还有…

Next.js Server Action 提交 vs 前端 Fetch 提交:核心区别与优劣分析

在使用 Next.js 开发时&#xff0c;开发者经常会面临一个问题&#xff1a;前端的数据提交应该直接 Fetch 调用 API 还是使用 Next.js 提供的 Server Action 提交&#xff1f; 本文将深度解析&#xff1a; ✅ Server Action 提交数据的工作原理✅ 前端 Fetch 提交数据的优缺点…

DeepSeek开启AI办公新模式,WPS/Office集成DeepSeek-R1本地大模型!

从央视到地方媒体&#xff0c;已有多家媒体机构推出AI主播&#xff0c;最近杭州文化广播电视集团的《杭州新闻联播》节目&#xff0c;使用AI主持人进行新闻播报&#xff0c;且做到了0失误率&#xff0c;可见AI正在逐渐取代部分行业和一些重复性的工作&#xff0c;这一现象引发很…

混合存储HDD+SSD机型磁盘阵列,配上SSD缓存功能,性能提升300%

企业日常运行各种文件无处不在&#xff0c;文档、报告、视频、应用数据......面对成千上万的文件&#xff0c;团队之间需要做到无障碍协作&#xff0c;员工能够即时快速访问、共享处理文件。随着业务增长&#xff0c;数字化办公不仅需要大容量&#xff0c;快速高效的文件访问越…

【AI】什么是Embedding向量模型?我们应该如何选择?

我们之前讲的搭建本地知识库,基本都是使用检索增强生成(RAG)技术来搭建,Embedding模型则是RAG的核心,同时也是大模型落地必不可少的技术。那么今天我们就来聊聊Embedding向量模型: 一、Embedding模型是什么? Embedding模型是一种将离散数据(如文本、图像、用户行为等)…

Java在小米SU7 Ultra汽车中的技术赋能

目录 一、智能驾驶“大脑”与实时数据 场景一&#xff1a;海量数据的分布式计算 场景二&#xff1a;实时决策的毫秒级响应 场景三&#xff1a;弹性扩展与容错机制 技术隐喻&#xff1a; 二、车载信息系统&#xff08;IVI&#xff09;的交互 场景一&#xff1a;Android Automo…

【Python 数据结构 8.串】

目录 一、串的基本概念 1.串的概念 2.获取串的长度 3.串的拷贝 4.串的比较 5.串的拼接 6.串的索引 二、Python中串的使用 1.串的定义 2.串的拼接 3.获取串的长度 4.获取子串位置 5.获取字符串的索引 6.字符串的切片 7.字符串反转 8.字符串的比较 9.字符串的赋值 三、实战 1.344…

计算机视觉cv2入门之图像的读取,显示,与保存

在计算机视觉领域&#xff0c;Python的cv2库是一个不可或缺的工具&#xff0c;它提供了丰富的图像处理功能。作为OpenCV的Python接口&#xff0c;cv2使得图像处理的实现变得简单而高效。 示例图片 目录 opencv获取方式 图像基本知识 颜色空间 RGB HSV 图像格式 BMP格式 …

LLM 学习(二 完结 Multi-Head Attention、Encoder、Decoder)

文章目录 LLM 学习&#xff08;二 完结 Multi-Head Attention、Encoder、Decoder&#xff09;Self-Attention &#xff08;自注意力机制&#xff09;结构多头注意力 EncoderAdd & Norm 层Feed Forward 层 EncoderDecoder的第一个Multi-Head AttentionMasked 操作Teacher Fo…

006-获取硬件序列号

获取硬件序列号 我将从跨平台角度系统讲解如何通过C获取硬件序列号的核心技术&#xff0c;并提供可移植性代码实现。 一、处理器序列号获取 Windows平台 #include <windows.h> #include <intrin.h>std::string GetCPUSerial_Win() {DWORD cpuInfo[2] { 0 };__c…

GDB调试技巧:多线程案例分析(保姆级)

在软件开发的复杂世界里&#xff0c;高效的调试工具是解决问题的关键利器。今天&#xff0c;我们将深入探讨强大的调试工具 ——GDB&#xff08;GNU Debugger&#xff09;。GDB 为开发者提供了一种深入程序内部运行机制、查找错误和优化性能的有效途径。让我们一同开启 GDB 的调…

OSPF的各种LSA类型,多区域及特殊区域

一、OSPF的LSA类型 OSPF&#xff08;开放最短路径优先&#xff09;协议使用多种LSA&#xff08;链路状态通告&#xff09;类型来交换网络拓扑信息。以下是主要LSA类型的详细分类及其作用&#xff1a; 1. Type 1 LSA&#xff08;路由器LSA Router LSA&#xff09; 生成者&…

JavaScript系列06-深入理解 JavaScript 事件系统:从原生事件到 React 合成事件

JavaScript 事件系统是构建交互式 Web 应用的核心。本文从原生 DOM 事件到 React 的合成事件&#xff0c;内容涵盖&#xff1a; JavaScript 事件基础&#xff1a;事件类型、事件注册、事件对象事件传播机制&#xff1a;捕获、目标和冒泡阶段高级事件技术&#xff1a;事件委托、…

字节跳动C++客户端开发实习生内推-抖音基础技术

智能手机爱好者和使用者&#xff0c;追求良好的用户体验&#xff1b; 具有良好的编程习惯&#xff0c;代码结构清晰&#xff0c;命名规范&#xff1b; 熟练掌握数据结构与算法、计算机网络、操作系统、编译原理等课程&#xff1b; 熟练掌握C/C/OC/Swift一种或多种语言&#xff…

MySQL进阶-关联查询优化

采用左外连接 下面开始 EXPLAIN 分析 EXPLAIN SELECT SQL_NO_CACHE * FROM type LEFT JOIN book ON type.card book.card; 结论&#xff1a;type 有All ,代表着全表扫描&#xff0c;效率较差 添加索引优化 ALTER TABLE book ADD INDEX Y ( card); #【被驱动表】&#xff0…

ai之qwq 32B部署在 linux 与拓展使用在web参考

linux部署 Linux 命令行&#xff1a; curl -fsSL https://ollama.com/install.sh | sh2 将Ollama设置为系统启动时自动运行&#xff08;建议&#xff09; 创建系统用户和用户组 sudo useradd -r -s /bin/false -U -m -d /usr/share/ollama ollamasudo usermod -a -G ollama $…

景联文科技:以精准数据标注赋能AI进化,构筑智能时代数据基石

在人工智能技术席卷全球的浪潮中&#xff0c;高质量数据已成为驱动AI模型进化的核心燃料。作为全球领先的AI数据服务解决方案提供商&#xff0c;景联文科技深耕数据标注领域多年&#xff0c;以技术为基、以专业为本&#xff0c;致力于为全球客户提供全场景、高精度、多模态的数…