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); //创建线程 1
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL); //创建线程 2
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	pthread_join(tid1,NULL); //阻塞回收线程 1
	pthread_join(tid2,NULL); //阻塞回收线程 2
	pthread_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);//创建线程1
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid2,NULL,fun2,NULL);//创建线程2
	if(ret != 0){
		perror("pthread_create");
		return -1;
	}
	ret = pthread_create(&tid3,NULL,fun3,NULL);//创建线程3
	if(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 操作来控制线程的执行顺序,如下图所示

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

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

相关文章

Mybatis-plus 内部提供的 ServiceImpl<M extends BaseMapper<T>, T> 学习总结

作用 当集成Mybatis-Plus 后&#xff0c;我们的大部分数据库操作都可以通过 XxxxxMapper &#xff0c;同时 Mybatis-plus 在Mapper 提供基本操作方法的同时&#xff0c;也提供类基础的 serviceImpl 来帮助我们完成一些常见的基本操作。 使用 一般情况下&#xff0c;我们首先…

Ipa Guard使用手册

使用手册 开始使用ipa guard代码混淆界面介绍文件混淆-界面介绍安装和登录Ipa Guard 相关教程 下载安装Ipa Guardipaguard注册和登录 下载安装Ipa Guard 可以前往ipaguard工具官网下载&#xff0c;工具是免费下载&#xff0c;免费体验使用的。下载地址是https://www.ipagua…

MS2358:96KHz、24bit 音频 ADC

MS2358 是带有采样速率 8kHz-96kHz 的立体声音频模数 转换器&#xff0c;适合于面向消费者的专业音频系统。 MS2358 通过使用增强型双位 Δ - ∑ 技术来实现其高精度 的特点。 MS2358 支持单端的模拟输入&#xff0c;所以不需要外部器 件&#xff0c;非常适合用于像 …

ChatGPT 宕机?OpenAI 将中断归咎于 DDoS 攻击

您的 ChatGPT 已关闭吗&#xff1f;您是否遇到 ChatGPT 问题&#xff0c;例如连接问题或遇到“长响应时出现网络错误”&#xff1f;– ChatGPT 遭受了一系列 DDoS 攻击&#xff0c;显然是由匿名苏丹组织策划的。 OpenAI 的 ChatGPT 是一款流行的人工智能聊天机器人&#xff0c;…

标本传送设备物联网应用案例|蓝蜂物联网一体化方案

标本传送设备物联网应用案例 标本传输系统被大量应用到现代医院场景中&#xff0c;系统各个设备的运行情况直接影响到整个医院系统的正常稳定&#xff0c;所以对于标本传输系统的实时监控和及时运维是维持医院稳定和规避风险的重中之重。 针对标本传输系统应用过程中的数据统…

ios安全加固 ios 加固方案

​ 目录 一、iOS加固保护原理 1.字符串混淆 2.类名、方法名混淆 3.程序结构混淆加密 4.反调试、反注入等一些主动保护策略 二 代码混淆步骤 1. 选择要混淆保护的ipa文件 2. 选择要混淆的类名称 3. 选择要混淆保护的函数&#xff0c;方法 4. 配置签名证书 5. 混淆和测…

centos7安装Nexus(Maven私服)与配置使用教程

之前有位大佬问我&#xff0c;他说有个第三方的Jar包&#xff0c;在idea导出库中使用&#xff0c;现在要部署上线测试&#xff0c;要如何导进去打包。 我说&#xff0c;不用那么麻烦&#xff0c;搞个Nexus私服&#xff0c;将Jar上传上去&#xff0c;然后配置Maven的setting文件…

现货黄金靠谱吗

现货黄金是一种相当不错的黄金投资方式&#xff0c;它通过紧密跟踪伦敦市场上黄金现货的价格走势&#xff0c;为投资者提供了捕捉差价的机会&#xff0c;而且在交易平台的帮助下&#xff0c;投资者可以获得很高的资金杠杆&#xff0c;因此即使只是抓住了一点点的行情波动&#…

01_ddim_inversion_CN

DDIM反转 设置 # !pip install -q transformers diffusers accelerateimport torch import requests import torch.nn as nn import torch.nn.functional as F from PIL import Image from io import BytesIO from tqdm.auto import tqdm from matplotlib import pyplot as p…

链表练习题

作者前言 &#x1f382; ✨✨✨✨✨✨&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f367;&#x1f382; ​&#x1f382; 作者介绍&#xff1a; &#x1f382;&#x1f382; &#x1f382; &#x1f389;&#x1f389;&#x1f389…

时间序列预测实战(十一)用SCINet实现滚动预测功能(附代码+数据集+原理介绍)

论文地址->SCINet官方论文地址 官方代码地址-> 官方代码下载地址 个人整理的代码地址->免费分享给大家创作不易请大家给文章点点赞 一、本文介绍 这篇文章给大家带来的是关于SCINet实现时间序列滚动预测功能的讲解&#xff0c;SCINet是样本卷积交换网络的缩写(Sam…

devops完整搭建教程(gitlab、jenkins、harbor、docker)

devops完整搭建教程&#xff08;gitlab、jenkins、harbor、docker&#xff09; 文章目录 devops完整搭建教程&#xff08;gitlab、jenkins、harbor、docker&#xff09;1.简介&#xff1a;2.工作流程&#xff1a;3.优缺点4.环境说明5.部署前准备工作5.1.所有主机永久关闭防火墙…

HTTPS的工作流程

. HTTPS是什么&#xff1f; https是应用层中的一个协议&#xff0c;是在http协议的基础上引入的一个加密层。 为什么需要HTTPS 由于http协议内容都是按照文本的方式明文传输的&#xff0c;这就导致传输过程中会出现一些被篡改的情况。运营商劫持事件最开始百度&#xff0c;…

比较PID控制和神经网络控制在机器人臂上的应用

机器人臂是自动化领域中常见的机器人形式&#xff0c;其精确控制对于实现复杂任务具有重要意义。在机器人臂的控制中&#xff0c;PID控制和神经网络控制是两种常用的控制方法。本文将比较PID控制和神经网络控制在机器人臂控制方面的应用&#xff0c;包括控制原理、优缺点以及在…

【广州华锐互动】太空探索VR模拟仿真教学系统

随着科技的不断发展&#xff0c;人类对宇宙的探索欲望愈发强烈。火星作为距离地球最近的行星之一&#xff0c;自然成为了人类关注的焦点。近年来&#xff0c;火星探测取得了一系列重要成果&#xff0c;为人类了解火星提供了宝贵的信息。然而&#xff0c;实地考察火星仍然面临着…

C++——基础

初学C的时候&#xff0c;有没有想过&#xff0c;为什么C支持重载&#xff0c;而C不支持重载呢&#xff1f;&#xff1f; 其实&#xff0c;一个程序运行起来都要经过四步骤 预处理编译汇编链接 预处理阶段会经过去注释&#xff0c;宏替换&#xff0c;头文件展开&#xff0c;条…

Liunx终极环境搭建

华子目录 网络服务准备工作安装RHEL9系统部署RHEL9操作系统虚拟网络编辑器配置RHEL9系统系统中的设置更换yum源修改主机名关闭selinux&#xff0c;firewalld设置静态ip &#xff08;网络配置&#xff09; 网络服务 准备工作 以下为RHEL9镜像资源&#xff0c;有需要的博友们可…

Ubuntu(WSL) mysql8.0.31 源码安装

要在 Ubuntu 上使用调试功能安装 MySQL 8.0 的源码&#xff0c;可以按照以下详细步骤进行操作&#xff1a; 1. 更新系统 首先&#xff0c;确保你的 Ubuntu 系统是最新的。运行以下命令更新系统软件包&#xff1a; sudo apt update sudo apt upgrade 2. 下载 MySQL 源码 访…

ChatGPT Plus的Vision升级是一个改变游戏规则的创举

内容来源&#xff1a;0xluffy_eth ChatGPT Plus的Vision升级是一个改变游戏规则的创举&#xff01; 现在每个用户都可以以每月20美元的价格雇用自己的个人数字助理实习生&#xff0c;具备VISION&#xff01; 以下是10个惊人的例子&#xff08;&#xff09; 1&#xff0c; 我…

Blender--》点线面操作及其面操作的详解

接下来我会在three.js专栏中分享关于3D建模知识的文章&#xff0c;如果学习three朋友并且想了解和学习3D建模&#xff0c;欢迎关注本专栏&#xff0c;关于这款3D建模软件blender的安装&#xff0c;我在前面的文章已经讲解过了&#xff0c;如果不了解的朋友可以去考考古&#xf…