Linux 多线程控制详解

目录

多线程编临界资源访问

互斥锁 API 简述

初始化互斥量

互斥量加锁/解锁

互斥量加锁(非阻塞方式)

互斥量销毁

程序示例

多线程编执行顺序控制

信号量 API 简述

初始化信号量

信号量 P/V 操作

信号量申请(非阻塞方式)

信号量销毁

程序示例

条件变量

创建和销毁条件变量

等待条件变量

通知条件变量

程序示例

总结

线程使用流程图

互斥量使用流程图

信号量使用流程图


多线程编临界资源访问

当线程在运行过程中,去操作公共资源,如全局变量的时候,可能会发生彼 此“矛盾”现象。

例如线程 1 企图想让变量自增,而线程 2 企图想要变量自减, 两个线程存在互相竞争的关系导致变量永远处于一个“平衡状态”,两个线程互相竞争,线程 1 得到执行权后将变量自加,当线程 2 得到执行权后将变量自减, 变量似乎永远在某个范围内浮动,无法到达期望数值

如例程 9 所示

测试例程 9:(Phtread_txex9.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>int Num = 0;void *fun1(void *arg)
{while(Num < 3){Num++;printf("%s:Num = %d\n",__FUNCTION__,Num);sleep(1);}pthread_exit(NULL);
}void *fun2(void *arg)
{while(Num > -3){Num--;printf("%s:Num = %d\n",__FUNCTION__,Num);sleep(1);}pthread_exit(NULL);
}int main()
{int ret;pthread_t tid1,tid2;ret = pthread_create(&tid1,NULL,fun1,NULL);if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid2,NULL,fun2,NULL);if(ret != 0){perror("pthread_create");return -1;}pthread_join(tid1,NULL);pthread_join(tid2,NULL);return 0;
}

运行结果:

为了解决上述对临界资源的竞争问题,pthread 线程引出了互斥锁来解决临界资源访问。通过对临界资源加锁来保护资源只被单个线程操作,待操作结束后解锁,其余线程才可获得操作权。

互斥锁 API 简述

多个线程都要访问某个临界资源,比如某个全局变量时,需要互斥地访问: 我访问时,你不能访问。

可以使用以下函数进行互斥操作。

初始化互斥量

函数原型如下:

int pthread_mutex_init(phtread_mutex_t *mutex, const pthread_mutexattr_t *restrict attr);

该函数初始化一个互斥量,第一个参数是改互斥量指针,第二个参数为控制互斥量的属性,一般为 NULL。当函数成功后会返回 0,代表初始化互斥量成功。

当然初始化互斥量也可以调用宏来快速初始化,代码如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;

互斥量加锁/解锁

函数原型如下:

互斥量加锁(阻塞)/解锁

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);

int pthread_mutex_unlock(pthread_mutex_t *mutex);

成功:返回 0

lock 函数与 unlock 函数分别为加锁解锁函数,只需要传入已经初始化好的 pthread_mutex_t 互斥量指针。成功后会返回 0。

当某一个线程获得了执行权后,执行 lock 函数,一旦加锁成功后,其余线 程遇到 lock 函数时候会发生阻塞,直至获取资源的线程执行 unlock 函数后。 unlock 函数会唤醒其他正在等待互斥量的线程。

特别注意的是,当获取 lock 之后,必须在逻辑处理结束后执行 unlock,否则会发生死锁现象!导致其余线程一直处于阻塞状态,无法执行下去。在使用互 斥量的时候,尤其要注意使用 pthread_cancel 函数,防止发生死锁现象!

互斥量加锁(非阻塞方式)

函数原型如下:

互斥量加锁(非阻塞)

#include <pthread.h>

int pthread_mutex_trylock(pthread_mutex_t *mutex);

该函数同样也是一个线程加锁函数,但该函数是非阻塞模式通过返回值来 判断是否加锁成功,用法与上述阻塞加锁函数一致。

互斥量销毁

函数原型如下:

互斥量销毁

#include <pthread.h>

int pthread_mutex_destory(pthread_mutex_t *mutex);

成功:返回 0

该函数是用于销毁互斥量的,传入互斥量的指针,就可以完成互斥量的销毁,成功返回 0。

程序示例

测试例程 10:(Phtread_txex10.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>pthread_mutex_t mutex; //互斥量变量 一般申请全局变量int Num = 0; //公共临界变量void *fun1(void *arg)
{pthread_mutex_lock(&mutex); //加锁 若有线程获得锁,则会阻塞while(Num < 3){Num++;printf("%s:Num = %d\n",__FUNCTION__,Num);sleep(1);}pthread_mutex_unlock(&mutex); //解锁pthread_exit(NULL); //线程退出 pthread_join 会回收资源
}void *fun2(void *arg)
{ pthread_mutex_lock(&mutex); //加锁 若有线程获得锁,则会阻塞while(Num > -3){Num--;printf("%s:Num = %d\n",__FUNCTION__,Num);sleep(1);}pthread_mutex_unlock(&mutex); //解锁pthread_exit(NULL); //线程退出 pthread_join 会回收资源
}int main()
{int ret;pthread_t tid1,tid2; ret = pthread_mutex_init(&mutex,NULL); //初始化互斥量if(ret != 0){perror("pthread_mutex_init");return -1;}ret = pthread_create(&tid1,NULL,fun1,NULL); //创建线程 1if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid2,NULL,fun2,NULL); //创建线程 2if(ret != 0){perror("pthread_create");return -1;}pthread_join(tid1,NULL); //阻塞回收线程 1pthread_join(tid2,NULL); //阻塞回收线程 2pthread_mutex_destroy(&mutex); //销毁互斥量return 0;
}

运行结果:

上述例程通过加入互斥量,保证了临界变量某一时刻只被某一线程控制, 实现了临界资源的控制。需要说明的是,线程加锁在循环内与循环外的情况。

本历程在进入 while 循环前进行了加锁操作,在循环结束后进行的解锁操作, 如果将加锁解锁全部放入 while 循环内,作为单核的机器,执行结果无异,当有多核机器执行代码时,可能会发生“抢锁”现象,这取决于操作系统底层的实现。

多线程编执行顺序控制

解决了临界资源的访问,但似乎对线程的执行顺序无法得到控制,因线程都是无序执行,之前采用 sleep 强行延时的方法勉强可以控制执行顺序,但此方法在实际项目情况往往是不可取的,其仅仅可解决线程创建的顺序,当创建之后执行的顺序又不会受到控制,于是便引入了信号量的概念,解决线程执行顺序。

测试例程 11:(Phtread_txex11.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>void *fun1(void *arg)
{printf("%s:Pthread Come!\n",__FUNCTION__);pthread_exit(NULL);
}void *fun2(void *arg)
{printf("%s:Pthread Come!\n",__FUNCTION__);pthread_exit(NULL);
}void *fun3(void *arg)
{printf("%s:Pthread Come!\n",__FUNCTION__);pthread_exit(NULL);
}int main()
{int ret;pthread_t tid1,tid2,tid3;ret = pthread_create(&tid1,NULL,fun1,NULL);if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid2,NULL,fun2,NULL);if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid3,NULL,fun3,NULL);if(ret != 0){perror("pthread_create");return -1;}pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);return 0;
}

运行结果:通过上述例程可以发现,多次执行该函数其次序是无序的,线程之间的竞 争无法控制,通过使用信号量来使得线程顺序为可控的。

信号量 API 简述

注意:信号量跟互斥量不一样,互斥量用来防止多个线程同时访问某个临界资源。信号量起通知作用,线程 A 在等待某件事,线程 B 完成了这件事后就 可以给线程 A 发信号。

初始化信号量

函数原型如下:

int sem_init(sem_t *sem,int pshared,unsigned int value);

  • 该函数可以初始化一个信号量,第一个参数传入 sem_t 类型指针;
  • 第二个参数传入 0 代表线程控制,否则为进程控制;
  • 第三个参数表示信号量的初始值,0 代表阻塞,1 代表运行。
  • 待初始化结束信号量后,若执行成功会返回 0。

信号量 P/V 操作

函数原型如下:

#include <pthread.h>

int sem_wait(sem_t *sem);

int sem_post(sem_t *sem);

成功:返回 0

  • sem_wait 函数作用为检测指定信号量是否有资源可用,若无资源可用会阻塞等待,若有资源可用会自动的执行“sem-1”的操作。所谓的“sem-1”是与上述 初始化函数中第三个参数值一致,成功执行会返回 0。
  • sem_post 函数会释放指定信号量的资源,执行“sem+1”操作。

通过以上 2 个函数可以完成所谓的 PV 操作,即信号量的申请与释放,完成 对线程执行顺序的控制。

信号量申请(非阻塞方式)

函数原型如下:

#include <pthread.h>

int sem_trywait(sem_t *sem);

成功:返回 0

此函数是信号量申请资源的非阻塞函数,功能与 sem_wait 一致,唯一区别在于此函数为非阻塞。

信号量销毁

函数原型如下:

#include <pthread.h>

int sem_destory(sem_t *sem);

成功:返回 0

该函数为信号量销毁函数,执行过后可将信号量进行销毁

程序示例

测试例程 12:(Phtread_txex12.c)

#define _GNU_SOURCE 
#include <pthread.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <semaphore.h>sem_t sem1,sem2,sem3;//申请的三个信号量变量void *fun1(void *arg)
{sem_wait(&sem1);//因sem1本身有资源,所以不被阻塞 获取后sem1-1 下次会会阻塞printf("%s:Pthread Come!\n",__FUNCTION__);sem_post(&sem2);// 使得sem2获取到资源pthread_exit(NULL);
}void *fun2(void *arg)
{sem_wait(&sem2);//因sem2在初始化时无资源会被阻塞,直至14行代码执行 不被阻塞 sem2-1 下次会阻塞printf("%s:Pthread Come!\n",__FUNCTION__);sem_post(&sem3);// 使得sem3获取到资源pthread_exit(NULL);
}void *fun3(void *arg)
{sem_wait(&sem3);//因sem3在初始化时无资源会被阻塞,直至22行代码执行 不被阻塞 sem3-1 下次会阻塞printf("%s:Pthread Come!\n",__FUNCTION__);sem_post(&sem1);// 使得sem1获取到资源pthread_exit(NULL);
}int main()
{int ret;pthread_t tid1,tid2,tid3;ret = sem_init(&sem1,0,1);  //初始化信号量1 并且赋予其资源if(ret < 0){perror("sem_init");return -1;}ret = sem_init(&sem2,0,0); //初始化信号量2 让其阻塞if(ret < 0){perror("sem_init");return -1;}ret = sem_init(&sem3,0,0); //初始化信号3 让其阻塞if(ret < 0){perror("sem_init");return -1;}ret = pthread_create(&tid1,NULL,fun1,NULL);//创建线程1if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2if(ret != 0){perror("pthread_create");return -1;}ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3if(ret != 0){perror("pthread_create");return -1;}/*回收线程资源*/pthread_join(tid1,NULL);pthread_join(tid2,NULL);pthread_join(tid3,NULL);/*销毁信号量*/sem_destroy(&sem1);sem_destroy(&sem2);sem_destroy(&sem3);return 0;
}

运行结果:

该例程加入了信号量,使得线程的执行顺序变为可控的。在初始化信号量时, 将信号量 1 填入资源,第一个线程调用 sem_wait 函数可以成功获得信号量,在 执行完逻辑后使用 sem_pos 函数来释放。当执行函数 sem_wait 后,会执行 sem 自减操作,使下一次竞争被阻塞,直至通过 sem_pos 被释放

上述例程因 38 行初始化信号量 1 时候,使其默认获取到资源;

第 43、48 行 初始化信号量 2、3 时候,使之没有资源。于是在线程处理函数中,每个线程通过 sem_wait 函数来等待资源,发生阻塞。因信号量 1 初始值为有资源,故可以 先执行线程 1 的逻辑。待执行完第 12 行 sem_wait 函数,会导致 sem1-1,使得 下一次此线程会被阻塞。继而执行至 14 行,通过 sem_post 函数使 sem2 信号量 获取资源,从而冲破阻塞执行线程 2 的逻辑...以此类推完成线程的有序控制。

条件变量

条件变量时一种同步机制,用来通知其他线程条件满足了。一般是用来通知对方共享数据的状态信息,因此条件变量是结合互斥量来使用的。

创建和销毁条件变量

函数原型如下:

#include <pthread.h>

// 初始化条件变量 pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

int pthread_cond_init(pthread_cond_t *cond, pthread_condattr_t *cond_attr);//cond_attr 通常为 NULL

// 销毁条件变量

int pthread_cond_destroy(pthread_cond_t *cond);

这些函数成功时都返回 0

等待条件变量

函数原型如下:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

这需要结合互斥量一起使用,示例代码如下:

pthread_mutex_lock(&g_tMutex);

// 如果条件不满足则,会 unlock g_tMutex

// 条件满足后被唤醒,会 lock g_tMutex pthread_cond_wait(&g_tConVar, &g_tMutex);

/* 操作临界资源 */

pthread_mutex_unlock(&g_tMutex);

通知条件变量

函数原型如下:

int pthread_cond_signal(pthread_cond_t *cond);

pthread_cond_signal 函数只会唤醒一个等待 cond 条件变量的线程,示例代码如下:

pthread_cond_signal(&g_tConVar);

程序示例

总结

线程使用流程图

有关多线程的创建流程如图 所示,首先需要创建线程,一旦线程创 建完成后,线程与线程之间会发生竞争执行,抢占时间片来执行线程逻辑。在 创建线程时候,可以通过创建线程的第四个参数传入参数,在线程退出时亦可 传出参数被线程回收函数所回收,获取到传出的参数。

互斥量使用流程图

当多个线程出现后,会遇到同时操作临界公共资源的问题,当线程操作公 共资源时需要对线程进行保护加锁,防止其与线程在此线程更改变量时同时更 改变量,待逻辑执行完毕后再次解锁,使其余线程再度开始竞争。互斥锁创建 流程下图所示。

信号量使用流程图

当多个线程出现后,同时会遇到无序执行的问题。有时候需要对线程的执行顺序做出限定,变引入了信号量,通过 PV 操作来控制线程的执行顺序,如下图所示

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

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

相关文章

node插件MongoDB(四)—— 库mongoose 的条件控制(三)

文章目录 前言一、运算符二、逻辑运算1. $or 逻辑或2. $and 逻辑与 三、正则匹配 前言 在mongodb 不能使用 > < > < ! 等运算符&#xff0c;需要使用替代符号。 一、运算符 > 使用 $gt< 使用 $lt> 使用 $gte< 使用 $lte! 使用 $ne 例子&#xff1a;获…

单片机启动流程

存储器 ​ 一个单片机中存在rom和ram&#xff0c;Soc也有rom和ram&#xff08;ddrx&#xff09;&#xff0c;部分Soc还包含MMU&#xff08;Memory Manage Unit 内存管理单元&#xff09;— &#xff08;用于系统内存管理&#xff0c;比如说虚拟内存空间&#xff0c;内存区间的…

Flink SQL自定义标量函数(Scalar Function)

使用场景&#xff1a; 标量函数即 UDF&#xff0c;⽤于进⼀条数据出⼀条数据的场景。 开发流程&#xff1a; 实现 org.apache.flink.table.functions.ScalarFunction 接⼝实现⼀个或者多个⾃定义的 eval 函数&#xff0c;名称必须叫做 eval&#xff0c;eval ⽅法签名必须是 p…

jenkins通知

构建失败邮件通知 配置自己的邮箱 配置邮件服务&#xff0c;密码是授权码 添加构建后操作 扩展 配置流水线 添加扩展 钉钉通知 Jenkins安装钉钉插件 钉钉添加机器人 加签 https://oapi.dingtalk.com/robot/send?access_token98437f84ffb6cd64fa2d7698ef44191d49a11…

为什么审计平台不适合进行数据库变更管理?

关于视源电子 广州视源电子科技股份有限公司 (CVTE) 成立于 2005 年 12 月&#xff0c;旗下拥有多家业务子公司。 截至 2022 年底&#xff0c;公司总人数超 6000 人&#xff0c;约 60% 为技术人员。公司的主营业务为液晶显示主控板卡和交互智能平板等显控产品的设计、研发与销…

C语言之文件操作(剩余部分)

上篇博客字数到极限了&#xff0c;给大家把内容补充在这一篇&#xff0c;我们还剩下文件读取结束的判定和文件缓冲区的内容没有介绍&#xff0c;让我们开始下面的学习吧&#xff01; 目录 1.文件读取结束的判定 1.1feof函数 1.2ferror函数 代码示例 2.文件缓冲区 2.1fflu…

Redis之主从复制

文章目录 一、什么是Redis主从复制&#xff1f;1.作用2.配置主从复制的原因3.环境配置 二、一主二从三、复制原理四、链路总结 一、什么是Redis主从复制&#xff1f; 主从复制&#xff0c;是指将一台Redis服务器的数据&#xff0c;复制到其他的Redis服务器。前者称为主节点(ma…

用Go实现网络流量解析和行为检测引擎

1.前言 最近有个在学校读书的迷弟问我:大德德, 有没有这么一款软件, 能够批量读取多个抓包文件,并把我想要的数据呈现出来, 比如:源IP、目的IP、源mac地址、目的mac地址等等。我说&#xff1a;“这样的软件你要认真找真能找出不少开源软件, 但毕竟没有你自己的灵魂在里面,要不…

类与对象(2)

✨前言✨ &#x1f4d8; 博客主页&#xff1a;to Keep博客主页 &#x1f646;欢迎关注&#xff0c;&#x1f44d;点赞&#xff0c;&#x1f4dd;留言评论 ⏳首发时间&#xff1a;2023年11月11日 &#x1f4e8; 博主码云地址&#xff1a;博主码云地址 &#x1f4d5;参考书籍&…

C++动态库

C动态库 动态库文件&#xff08;Dynamic Link Library&#xff0c;DLL&#xff09;是程序在运行时所需要调用的库。静态库文件是程序在编译时所需要调用的库。 1 环境介绍 VS版本&#xff1a;VS2017 编程语言&#xff1a;C 2 功能介绍 使用VS2017项目模板创建C动态库生成…

Java 之 IO/NIO/OKIO

BIO blocking io AIO Asynchronous IO 从内存读取到写入--输出 从外部到内存 -- 输入 OutputStream //文件不存在则自动创建 try {OutputStream outputStream new FileOutputStream("text.txt");outputStream.write(a);outputStream.write(b);} catch (IOExcep…

Java-多态

1. 多态 1.1 多态的概念 多态的概念&#xff1a;通俗来说&#xff0c;就是多种形态&#xff0c;具体点就是去完成某个行为&#xff0c;当不同的对象去完成时会产生出不同的状态。 1.2 多态实现条件 在java中要实现多态&#xff0c;必须要满足如下几个条件&#xff0c;缺一不…

vivado时序分析-3时序分析关键概念

1、时钟相移 时钟相移对应于延迟时钟波形 &#xff0c; 此波形与因时钟路径内的特殊硬件所导致的参考时钟相关。在 AMD FPGA 中 &#xff0c; 时钟相移通常是由 MMCM 或 PLL 原语引入的 &#xff0c; 前提是这些原语的输出时钟属性 CLKOUT*_PHASE 为非零值。 时序分析期间…

Linux 基于 LVM 逻辑卷的磁盘管理【简明教程】

一、传统磁盘管理的弊端 传统的磁盘管理&#xff1a;使用MBR先对硬盘分区&#xff0c;然后对分区进行文件系统的格式化最后再将该分区挂载上去。 传统的磁盘管理当分区没有空间使用进行扩展时&#xff0c;操作比较麻烦。分区使用空间已经满了&#xff0c;不再够用了&#xff…

如何使用HadSky搭配内网穿透工具打造个人站点并公网访问

&#x1f308;个人主页&#xff1a;聆风吟 &#x1f525;系列专栏&#xff1a;Cpolar杂谈、数据结构、算法模板 &#x1f516;少年有梦不应止于心动&#xff0c;更要付诸行动。 文章目录 前言一. 网站搭建1.1 网页下载和安装1.2 网页测试1.3 cpolar的安装和注册 二. 本地网页发…

【算法 | 模拟No.4】AcWing 756. 蛇形矩阵 AcWing 40. 顺时针打印矩阵

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【手撕算法系列专栏】【AcWing算法提高学习专栏】 &#x1f354;本专栏旨在提高自己算法能力的同时&#xff0c;记录一下自己的学习过程&a…

【chat】4: ubuntu20.04:数据库创建:mysql8 导入5.7表

【chat】3: ubutnu 安装mysql-8 并支持远程访问 已经支持 8.0的SQLyog 远程访问:大神2021年的文章:sql是5.7的版本,我使用的ubuntu20.04,8.0版本:chat数据库设计 C++搭建集群聊天室(七):MySQL数据库配置 及项目工程目录配置 User表,以id 唯一标识 Friend 表,自己的id…

JavaFX入门和网格布局面板的使用,Dao层交互,舞台与场景切换以及其他控件的使用

网格布局 将整个面板划分为若干个格子 , 每个格子的大小是一样的 , 每个格子中可以放置一个控件&#xff08;布局&#xff09; , 类似于表格的方式。在网格布局 中放入控件的时候 , 还需要指定位置。 GridPane gridPane new GridPane(); 我们将要排出这个布局 , 也就是登陆页…

如何通过命令查看某一文件的内容改动和提交记录

1. 查看最近10条的提交记录 一行显示 git log --oneline -102.查看某一个文件的提交记录 git log --oneline -10 文件路径3.查看某个文件的修改内容 查看某次提交的修改 内容 git show bcd9299 查看某次提交某个文件的修改内容git show bcd9299 文件路径4.对比两次提交内容的…

【STM32】HAL库UART含校验位的串口通信配置BUG避坑

【STM32】HAL库UART含校验位的串口通信配置BUG避坑 文章目录 UART协议校验位HAL库配置含校验位的串口配置BUG避坑附录&#xff1a;Cortex-M架构的SysTick系统定时器精准延时和MCU位带操作SysTick系统定时器精准延时延时函数阻塞延时非阻塞延时 位带操作位带代码位带宏定义总线函…