【Linux】线程同步

线程同步

线程仅仅互斥,是可以保证线程安全的。但是,这不合理!如果一个线程竞争到了锁,那么再它释放后它依然可以竞争个锁。因为CPU此时正在执行当前线程,所以该线程又可以继续竞争锁。 这样就会造成一个问题,有5个线程抢10000张票,可是线程1就抢了9000张,线程2抢了1000张。这就会造成其他另外三个线程一直申请锁却申请不到的情况。这个问题也被称为饥饿问题。

举个例子:

一个自习室只有一把钥匙,而每次这个自习室只能待一个人,且这个人持有钥匙。有一天你凌晨2点就起来去抢自习室,你抢到了并把自习室钥匙放口袋里进去自习,这时外面逐渐有人来自习,但是他们没有这个自习室的钥匙。所以他们只能在外面等待,这时你突然想出去吃个饭,上个厕所。那么你带着钥匙出去,并在外面把门反锁。这种行为就是线程持有锁挂起了。 而不久后你回到自习室,不太想学了,所以你就走到门口,把钥匙挂到墙上。可是你突然转念一想 : “算了,还是再学会吧。”。因为此时你离这个钥匙最近,所以你竞争的能力非常强。你就又拿到钥匙,进去自习了一份,你又想走了。于是又来到门口把钥匙挂门口隔壁的墙上,然后刚挂上去,你又想继续学了。又把钥匙拿下来继续进去学习… 如此反复。就造成了门外等待自习室的人的饥饿问题。而当你学完时真正准备走时,你把钥匙挂墙上后。结果外面的人争先恐后,毫无秩序的冲过来抢夺钥匙。

这里的你和外面自习室的人都是一个个线程,而钥匙就是这把锁,自习室就是临界资源。只有持有锁才能访问临界资源。可是因为锁刚从你当前的线程中释放,那么竞争锁能力最强的也是你当前的线程。 那么这样就很容易会造成其他线程的饥饿问题。 但是这个线程它有错吗??? 它没错!但是这不合理!!

那么怎么让这合理起来呢?

要合理起来,那么就让释放锁的线程,跑到最后面去排队。 还是上面那个例子,一旦你把钥匙挂墙上。那么你就必须老老实实到后面排队。这样可以确保一定的顺序时,但是还会面临一个问题。 有人插队怎么办???

从线程的角度来说,线程A已经把锁释放了。随后线程A跑到了最后的位置等待调度。这样可以保证一定的顺序的问题。接下来CPU准备调度线程B了,可是线程C它想配合,直接插队到了线程B前面。所以CPU就先调度线程C了。

CPU,线程A,B,C,它们有错吗? CPU只负责调度,谁来了调度谁,CPU没错。A,B,C线程都在尽心尽力的竞争锁。因为锁本来就是临界资源,它们也没错。但是,这不合理!!

于是就有了条件变量,可以保证线程同步。

条件变量

条件变量如何保证线程同步呢?还是自习室的例子,当你在自习室。外面等待的人都在睡觉(线程挂起),当你要走了挂回钥匙时(释放锁),外面的人就全部醒过来(线程唤醒)竞争锁。那么我们不要让他们全部醒过来,那么我们加一个管理员,让管理员每次只喊醒队伍最前面的那个,然后再让刚刚退出自习室的人到最后面去排队。 这样依次下去,是不是就能保证一定的顺序性了?

同步的情况下,想要每个线程先竞争锁 -> 竞争锁后检测是否满足访问临界资源的条件 -> 满足则访问 -> 不满足就在条件变量下等待 -> 等待之前释放锁 -> 唤醒回来重新获得锁 -> 访问临界资源

而让线程等待和唤醒线程。我们都需要用到条件变量。

条件变量和锁一样是一个变量,我们可以用条件变量让线程在该条件变量下等待。然后让主线程去唤醒条件变量。

条件变量相关函数:

pthread_cond_t cond = PTHREAD_COND_INITIALIZER; //如果条件变量是全局的,可以这样初始化//初始化全局变量,第一个参数是条件变量的地址,第二参数是条件变量的属性
int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);//销毁条件变量,传条件变量地址
int pthread_cond_destroy(pthread_cond_t *cond);//在条件变量下等待,第一个参数是条件变量地址,第二个参数是锁的地址,第三个参数是要等待的时间
int pthread_cond_timedwait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex,const struct timespec *restrict abstime);//在条件变量下等待,第一个参数是要等待的条件变量地址,第二个参数是锁
int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);//依次唤醒所有在条件变量下等待的线程
int pthread_cond_broadcast(pthread_cond_t *cond);//唤醒单个在条件变量下等待的线程
int pthread_cond_signal(pthread_cond_t *cond);

代码测试:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>#define TNUM 4typedef void (*func_t)(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond);int tickets = 5;
bool flag = true;class ThreadData
{
public:ThreadData(const std::string &name, func_t func, pthread_mutex_t *mutex, pthread_cond_t *cond) : _name(name), _func(func), _mutex(mutex), _cond(cond) {}public:std::string _name;func_t _func;pthread_mutex_t *_mutex;pthread_cond_t *_cond;
};void func1(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_mutex_lock(mutex);pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....下载" << std::endl; pthread_mutex_unlock(mutex);}
}
void func2(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_mutex_lock(mutex);pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....查看用户" << std::endl; pthread_mutex_unlock(mutex);}
}
void func3(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_mutex_lock(mutex);pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....扫描" << std::endl; pthread_mutex_unlock(mutex);}
}
void func4(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_mutex_lock(mutex);pthread_cond_wait(cond, mutex); //在条件变量下等待std::cout<< name  << " runing ....广播" << std::endl; pthread_mutex_unlock(mutex);}
}void *Entry(void *args)
{ThreadData *td = (ThreadData *)args;td->_func(td->_name, td->_mutex, td->_cond); //调用线程绑定的函数delete td;return nullptr;
}int main()
{pthread_mutex_t mtx; // 互斥锁pthread_cond_t cond; // 条件变量pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);func_t funs[4] = {func1, func2, func3, func4}; //函数指针数组,存储上面4个函数pthread_t tids[4];// 创建线程for (int i = 0; i < TNUM; i++){std::string name = "thread ";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, funs[i], &mtx, &cond); //创建线程数据对象,存储线程的数据以及锁,条件变量信息pthread_create(tids + i, nullptr, Entry, (void *)td);}sleep(5);int cnt = 10;while (cnt){//每隔一秒唤醒一个线程std::cout << "wakeup thread ......  " << cnt--  << std::endl; pthread_cond_signal(&cond); //唤醒一个线程sleep(1);}   std::cout << "ctrl done" << std::endl;flag = false; //结束线程内的循环//走到这里,所有线程依旧处于wait状态,在这里需要再唤醒一次pthread_cond_broadcast(&cond); //唤醒所有线程for (int i = 0; i < TNUM; i++){pthread_join(tids[i], nullptr);std::cout << "thread " << i + 1 << "   quit....." << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

这个代码的逻辑是1个主线程负责唤醒条件变量下等待的线程。其他四个线程负责输出一条打印语句,然后进入等待。

运行结果:

在这里插入图片描述

我们可以发现明显的顺序性,但第一次的顺序是无法确定的。因为CPU先调度的线程会先等待,先等待的会被先唤醒。但是后面的次序都和第一次的次序一样。这就保证了线程的同步。

注意!!!

pthread_cond_wait 必须在加锁和解锁之间等待!!因为pthread_cond_wait函数会让线程在等待之前释放锁,其而让其他线程进入临界资源。等到被唤醒时,又会重新获取锁。这也就是为什么 要在加锁和解锁之间wait。如果不在加锁和解锁之间wait,那么在最后想要在唤醒所有线程的时候就会产生死锁!!因为pthread_cond_wait的第二个参数就是一把锁,wait后会释放锁,被唤醒后重新获得锁。所以当最后一次唤醒时,被唤醒的线程就持有锁结束了。而其他线程就会在条件变量下等待锁,但是持有锁的线程已经释放了。所以就产生了死锁。

错误代码代表:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>#define TNUM 2typedef void (*func_t)(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond);int tickets = 5;
bool flag = true;class ThreadData
{
public:ThreadData(const std::string &name, func_t func, pthread_mutex_t *mutex, pthread_cond_t *cond) : _name(name), _func(func), _mutex(mutex), _cond(cond) {}public:std::string _name;func_t _func;pthread_mutex_t *_mutex;pthread_cond_t *_cond;
};void func1(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){//wait不加锁pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....下载" << std::endl; }
}
void func2(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){//wait不加锁pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....查看用户" << std::endl; }
}void *Entry(void *args)
{ThreadData *td = (ThreadData *)args;td->_func(td->_name, td->_mutex, td->_cond); //调用线程绑定的函数delete td;return nullptr;
}int main()
{pthread_mutex_t mtx; // 互斥锁pthread_cond_t cond; // 条件变量pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tids[TNUM];func_t funs[TNUM] = {func1,func2};// 创建线程for (int i = 0; i < TNUM; i++){std::string name = "thread ";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, funs[i], &mtx, &cond); //创建线程数据对象,存储线程的数据以及锁,条件变量信息pthread_create(tids + i, nullptr, Entry, (void *)td);}sleep(5);int cnt = 10;while (cnt){//每隔一秒唤醒一个线程std::cout << "wakeup thread ......  " << cnt--  << std::endl; pthread_cond_signal(&cond); //唤醒一个线程sleep(1);}   std::cout << "ctrl done" << std::endl;flag = false; //结束线程内的循环//走到这里,所有线程依旧处于wait状态,在这里需要再唤醒一次pthread_cond_broadcast(&cond); //唤醒所有线程// pthread_cond_signal(&cond); //唤醒一个线程//pthread_cond_signal(&cond); //唤醒一个线程std::cout << "--------------------------" << std::endl;for (int i = 0; i < TNUM; i++){pthread_join(tids[i], nullptr);std::cout << "thread " << i + 1 << "   quit....." << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

最后的运行结果就是10秒后最后一次唤醒时,线程1持有锁结束了。而线程2还在等待锁被唤醒,所以就变成了死锁。这时候就无法跳出来了。

在这里插入图片描述

而在循环内之所可以那是因为在singal之后。wait又重新获得了锁,然后经过了一次循环又来到wait,wait等待前会先释放锁。所以这时候其他线程又可以争夺锁,但是在最后一次的时候,线程1singal后不再wait。那么也就是在前一次wait之后获得了锁,随后线程结束时还持有锁,而线程2还在等待锁被唤醒。

而在线程结束前释放锁,又可以正常结束了:

#include <iostream>
#include <pthread.h>
#include <unistd.h>
#include <string>#define TNUM 2typedef void (*func_t)(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond);int tickets = 5;
bool flag = true;class ThreadData
{
public:ThreadData(const std::string &name, func_t func, pthread_mutex_t *mutex, pthread_cond_t *cond) : _name(name), _func(func), _mutex(mutex), _cond(cond) {}public:std::string _name;func_t _func;pthread_mutex_t *_mutex;pthread_cond_t *_cond;
};void func1(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....下载" << std::endl; pthread_mutex_unlock(mutex); //临走前释放锁}
}
void func2(const std::string &name, pthread_mutex_t *mutex, pthread_cond_t *cond)
{while (flag){pthread_cond_wait(cond, mutex);  //在条件变量下等待std::cout<< name  << " runing ....查看用户" << std::endl; pthread_mutex_unlock(mutex);//临走前释放锁}
}void *Entry(void *args)
{ThreadData *td = (ThreadData *)args;td->_func(td->_name, td->_mutex, td->_cond); //调用线程绑定的函数delete td;return nullptr;
}int main()
{pthread_mutex_t mtx; // 互斥锁pthread_cond_t cond; // 条件变量pthread_mutex_init(&mtx, nullptr);pthread_cond_init(&cond, nullptr);pthread_t tids[TNUM];func_t funs[TNUM] = {func1,func2};// 创建线程for (int i = 0; i < TNUM; i++){std::string name = "thread ";name += std::to_string(i + 1);ThreadData *td = new ThreadData(name, funs[i], &mtx, &cond); //创建线程数据对象,存储线程的数据以及锁,条件变量信息pthread_create(tids + i, nullptr, Entry, (void *)td);}sleep(5);int cnt = 10;while (cnt){//每隔一秒唤醒一个线程std::cout << "wakeup thread ......  " << cnt--  << std::endl; pthread_cond_signal(&cond); //唤醒一个线程sleep(1);}   std::cout << "ctrl done" << std::endl;flag = false; //结束线程内的循环//走到这里,所有线程依旧处于wait状态,在这里需要再唤醒一次pthread_cond_broadcast(&cond); //唤醒所有线程//pthread_cond_signal(&cond); //唤醒一个线程//pthread_cond_signal(&cond); //唤醒一个线程std::cout << "--------------------------" << std::endl;for (int i = 0; i < TNUM; i++){pthread_join(tids[i], nullptr);std::cout << "thread " << i + 1 << "   quit....." << std::endl;}pthread_mutex_destroy(&mtx);pthread_cond_destroy(&cond);return 0;
}

在这里插入图片描述

但是!!这是一种错误的写法!!请务必哪里有加锁,就哪里有解锁,wait在加锁和解锁之间完成。因为wait是对临界资源的条件检测,所以wait本身也应该在临界区之内。

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

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

相关文章

Python 继承和子类示例:从 Person 到 Student 的演示

继承允许我们定义一个类&#xff0c;该类继承另一个类的所有方法和属性。父类是被继承的类&#xff0c;也叫做基类。子类是从另一个类继承的类&#xff0c;也叫做派生类。 创建一个父类 任何类都可以成为父类&#xff0c;因此语法与创建任何其他类相同&#xff1a; 示例&…

简单介绍一下迁移学习

迁移学习是一种机器学习技术&#xff0c;旨在利用从一个任务或领域学习到的知识来改善另一个任务或领域的学习性能。在传统的机器学习方法中&#xff0c;通常假设训练数据和测试数据是从相同的分布中独立同分布采样的。然而&#xff0c;在现实世界中&#xff0c;这个假设并不总…

Golang Websocket框架:实时通信的新选择

前言 在现代应用程序中&#xff0c;实时通信已经成为了一种必需的特性。而Websocket是一种在客户端和服务器之间建立持久连接的协议&#xff0c;可以实现实时的双向通信。Golang作为一门高效且简洁的语言&#xff0c;也提供了一些优秀的Websocket框架&#xff0c;方便开发者构…

【计算机网络】从输入URL到页面都显示经历了什么??

文字总结 ① DNS 解析&#xff1a;当用户输入一个网址并按下回车键的时候&#xff0c;浏览器获得一个域名&#xff0c;而在实际通信过程中&#xff0c;我们需要的是一个 IP 地址&#xff0c;因此我们需要先把域名转换成相应 IP 地址。浏览器会首先从缓存中找是否存在域名&…

一文5000字从0到1使用Jmeter实现轻量级的接口自动化测试(图文并茂)

接口测试虽然作为版本的一环&#xff0c;但是也是有一套完整的体系&#xff0c;有接口的功能测试、性能测试、安全测试&#xff1b;同时&#xff0c;由于接口的特性&#xff0c;接口的自动化低成本高收益的&#xff0c;使用一些开源工具或一些轻量级的方法&#xff0c;在测试用…

uniapp进行条件编译的两种方法

在UniApp中&#xff0c;进行条件编译有两种方法&#xff1a;使用process.env全局变量和使用条件注释。 1.使用process.env全局变量&#xff1a;UniApp支持根据不同的环境变量来进行条件编译。可以通过在代码中使用process.env来判断当前环境并执行相应的逻辑。 if(process.env…

Tauri2 mobile development traps

时间点&#xff1a;2023/10/24。最近在倒腾移动端开发&#xff0c;不想学原生和 flutter&#xff0c;试了试 react-native&#xff0c;开发体验没有想象中的舒服&#xff0c;干脆直接上 tauri 2 吧&#xff0c;半年前就听说 tauri 2 支持移动端&#xff0c;到现在应该可堪小用。…

idea 插件 checkstyle 规则示例和说明

idea 安装插件 idea 配置插件 checkstyle.xml 示例和说明 <?xml version"1.0"?> <!DOCTYPE module PUBLIC"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN""https://checkstyle.org/dtds/configuration_1_3.dtd"><mod…

Are Large Language Models Really Robust to Word-Level Perturbations?

本文是LLM系列文章&#xff0c;针对《Are Large Language Models Really Robust to Word-Level Perturbations?》的翻译。 大型语言模型真的对单词级扰动具有鲁棒性吗&#xff1f; 摘要1 引言2 相关工作3 合理稳健性评价的奖励模型&#xff08;TREvaL&#xff09;4 LLM的词级…

I/O软件层次结构(用户层软件,设备独立性软件,设备驱动程序,中断处理程序,硬件)

文章目录 1.用户层软件2.设备独立性软件1.主要实现的功能2.逻辑设备表&#xff08;LUT&#xff09; 3.设备驱动程序4.中断处理程序2.中断处理程序的处理流程 4.硬件 1.用户层软件 用户层软件实现了与用户交互的接口&#xff0c;用户可直接使用该层提供的、与I/O操作相关的库函数…

ubuntu gcc版本降级 Reset gcc version from 11.3 to 11.2 on Ubuntu 22.04

aptitude 需要自己安装 sudo apt-get install aptitude # 安装# aptitude的一些常用的操作&#xff1a; sudo aptitude update # 更新软件源 sudo aptitude search 软件名称 # 查看软件 sudo aptitude install 软件名称 …

【Qt】绘图与绘图设备

文章目录 绘图设备QPainter绘图实例案例1案例2-高级设置案例3&#xff1a;利用画家画资源图片 点击按钮移动图片 QtPaintDevice实例Pixmap绘图设备QImage 绘图设备QPicture 绘图设备 QPainter绘图 Qt 的绘图系统允许使用相同的 API 在屏幕和其它打印设备上进行绘制。整个绘图系…

Apifox创建团队 项目 接口 邀请成员步骤演示

我们打开Apifox 找到 个人空间 然后 点击新建团队 然后这里 我们输入名字 点击确定 我们的团队就出来了 然后 我们点击新建项目 然后肯定是 http 项目名称输入一下 然后 语言 我们中国肯定是中文的 然后点击确定 建好之后 我们就会进入自己的项目啦 然后 我们可以新建个接…

ThinkPHP8学习笔记

ThinkPHP8官方文档地址&#xff1a;ThinkPHP官方手册 一、composer换源 1、查看 composer 配置的命令composer config -g -l 2、禁用默认源镜像命令composer config -g secure-http false 3、修改为阿里云镜像源composer config -g repo.packagist composer https://mirror…

C51--单片机中断

51单片机是单线程模式&#xff0c;需要用到硬件中断。 一、中断系统 中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。 当中央处理器CPU正在处理某件事的时候&#xff0c;外界发生了紧急事件请求&#xff0c;要求CPU暂停当前工作&#xff0c;转而去处理这个紧急…

程序员们平时都喜欢逛什么论坛呢?

网站不在多&#xff0c;好用就行&#xff1b;技术不求精&#xff0c;好摸鱼就行。是时候祭出我收藏夹里的这15个网站了&#xff01; 求职必备&#xff1a;牛客网 https://www.nowcoder.com/ 年少不知牛客好&#xff0c;等到要面试的时候才发现是神器。 你可以在牛客上搜索到一…

C#WPFPrism框架导航应用实例

本文实例演示C#WPFPrism框架导航应用实例。 一、导航实现步骤 首先创建WPF项目,修改App相关文件内容,以便使用prism。 承接上一个模块化的实例,在这个基础上更改增加导航功能。 1.1首先在ModuleA中添加ViewModels文件夹,添加ViewAViewModel.cs类 如果想上下文自动查找…

ArcGIS中批量mxd高版本转低版本

我们经常在给别人发ArcGIS的工程文件mxd&#xff0c;结果到别人那发现mxd工程文件打不开&#xff0c;原因是我们的arcgis版本高于别人&#xff0c;此时工程文件又很多&#xff0c;一个个转存成低版本又嫌麻烦&#xff0c;于是我们做了个批量mxd高版本转低版本的小工具&#xff…

论文解读:Large Language Models as Analogical Reasoners

一、动机 大模型在各种类型的NLP任务上均展现出惊艳的表现。基于CoT propmt能够更好地激发大模型解决复杂推理问题的能力&#xff0c;例如解决数学解题&#xff0c;可以让模型生成reasoning path。现有的经典的CoT方法有few-shot cot、zero-shot cot等。然后现有的cot面临两个…

Transformer模型 | 用于目标检测的视觉Transformers训练策略

基于视觉的Transformer在预测准确的3D边界盒方面在自动驾驶感知模块中显示出巨大的应用,因为它具有强大的建模视觉特征之间远程依赖关系的能力。然而,最初为语言模型设计的变形金刚主要关注的是性能准确性,而不是推理时间预算。对于像自动驾驶这样的安全关键系统,车载计算机…