线程(五)线程的同步和互斥——线程信号量

文章目录

  • 线程
    • 线程的同步和互斥
      • 线程的同步和互斥--线程信号量
        • 示例--使用线程信号量来控制线程执行的先后顺序
        • 示例--使用信号量实现线程之间的互斥
        • 示例--使用信号量实现线程之间的同步
      • 死锁
      • 线程状态转换

线程

线程的同步和互斥

线程的同步和互斥–线程信号量

上边讲了互斥的方式互斥锁、读写锁、自旋锁,同步的方式条件变量,实现线程之间的同步和互斥还有一种方式–信号量

  • 信号量从本质上是一个非负整数计数器,是共享资源的数目,通常被用来控制对共享资源的访问。

  • 信号量可以实现线程的同步和互斥

  • 通过sem_post()sem_wait()函数对信号量进行加减操作从而解决线程的同步和互斥。

  • 信号量数据类型:sem_t

    案例:现图书馆购入10本Unix环境高级编程供学生借阅,这里的数量10就是共享资源的数量即信号量,如果有10位同学借走了这10本书,那么第11位同学想要再借阅的话就必须等待之前的10位同学中的任意一位将书归还否则它只能等待,这里的借书和还书就对应sem_wait()sem_post()两个函数,通过控制信号量的值就可以实现线程之间的同步和互斥。

信号量的初始化和销毁

#include <semaphore.h>int sem_init(sem_t *sem, int pshared, unsigned value);
int sem_destroy(sem_t *sem);/*功能:sem_init	对信号量进行初始化sem_destroy	对信号量进行销毁参数:sem		指向信号量的指针pshared		是否在进程间共享的标志,0为不共享(只在当前进程的多个线程中使用),1为共享(可以在多个进程中的多个线程中使用)value	信号量的初始值返回值:成功执行返回0,否则返回错误编码	
*/

信号量的加和减操作

#include <semaphore.h>int sem_post(sem_t *sem);
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);/*功能:sem_post			增加信号量的值sem_wait	 	减少信号量的值sem_trywait	 sem_wait()的非阻塞版本参数:sem			指向信号量的指针返回值:成功执行返回0,否则返回错误编码
*/
  • 调用sem_post()一次信号量作加1操作
  • 调用sem_wait()一次信号量作减1操作
  • 当线程调用sem_wait()后,若信号量的值小于0则线程阻塞。只有其他线程在调用sem_post()对信号量作加1操作后并且其值大于或等于0时,阻塞的线程才能运行(也就是说:当此时信号量的值为0时,若调用sem_wait()对信号量作减1操作,那么信号量的值小于0线程阻塞,此时必须等待另外的线程调用sem_post()函数对信号量作加1操作才能够使得被阻塞的线程能够对信号量作减1操作)。
示例–使用线程信号量来控制线程执行的先后顺序
#include "header.h"sem_t sem1;
sem_t sem2;void* exec_func1(void *arg)
{sem_wait(&sem1);printf("[thread id:%lx] [fun:func1] is running\n",pthread_self());pthread_exit(NULL);
}void* exec_func2(void *arg)
{sem_wait(&sem2);        //对信号量进行减1操作,如果减1后值小于0就阻塞等待其他线程调用sem_post对信号量加1操作printf("[thread id:%lx] [fun:func2] is running\n",pthread_self());sem_post(&sem1);    //唤醒被阻塞的线程1pthread_exit(NULL);
}void* exec_func3(void *arg)
{printf("[thread id:%lx] [fun:func3] is running\n",pthread_self());sem_post(&sem2);//对信号量的值加1,使得被此信号量阻塞的线程能够运行pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2, func3;//初始化信号量,信号量的值为0,只在此进程中的多个线程中共享sem_init(&sem1, 0, 0); sem_init(&sem2, 0, 0);  if((err = pthread_create(&func1, NULL, exec_func1, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func3, NULL, exec_func3, NULL)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_join(func3, NULL);sem_destroy(&sem1);      //销毁信号量sem_destroy(&sem2);return 0;
}

image-20241009101658153

通过编译执行可以发现使用线程信号量可以用来控制线程执行的先后顺序,代码在刚开始使用sem_init()函数将信号量的值初始化为0,所以一旦线程func1调用sem_wait()函数对信号量进行减1操作就会被阻塞,线程func2也是同样的道理,它们都在等待某一个线程通过调用sem_post()函数对信号量执行加1操作来唤醒被信号量阻塞的线程。通过线程func3调用sem_post()函数来唤醒线程func2,线程func2调用sem_post()函数来唤醒线程func1来控制线程之间的执行顺序。

示例–使用信号量实现线程之间的互斥
//account.c#include "account.h"
#include "header.h"Account* create_account(int acc_num, double balance)		//创建账户
{Account *a = (Account*)malloc(sizeof(Account));assert(a != NULL);a->acc_num = acc_num;a->balance = balance;//pthread_mutex_init(&a->mutex, NULL);sem_init(&a->sem, 0, 1);		//初始化线程信号量,信号量值为1return a;
}double withdrawal(Account *a, double amount)
{assert(a != NULL);//P(1)操作,对信号量作减1操作	sem_wait(&a->sem);
//	pthread_mutex_lock(&a->mutex);if(amount <= 0 || amount > a->balance){//V(1)操作,对信号量作加1操作sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return 0.0;}double balance = a->balance;sleep(1);		//模拟ATM机延迟balance -= amount;a->balance = balance;		//将余额balance取出amount后再存放回a账户                           //V(1)操作,对信号量作加1操作sem_post(&a->sem);//	pthread_mutex_unlock(&a->mutex);return amount;
}double deposit(Account *a, double amount)
{assert(a != NULL);sem_wait(&a->sem);//pthread_mutex_lock(&a->mutex);if(amount <= 0){sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return 0.0;}double balance = a->balance;sleep(1);balance += amount;a->balance = balance;sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return amount;
}double get_balance(Account *a)
{assert(a != NULL);sem_wait(&a->sem);//pthread_mutex_lock(&a->mutex);double balance = a->balance;sem_post(&a->sem);//pthread_mutex_unlock(&a->mutex);return balance;
}void destroy_account(Account *a)
{assert(a != NULL);//pthread_mutex_destroy(&a->mutex);sem_destroy(&a->sem);		//销毁线程信号量free(a);	//将在堆上开辟出来的空间释放a = NULL;
}

image-20241009105543329

通过编译执行可以发现通过信号量也能够像之前的互斥锁那样实现线程之间的互斥,在执行对应的函数时对信号量进行减1操作,然后再执行完后对信号量进行加1操作。这样如果中间被别的线程打断执行,那么由于再次执行减1操作后信号量的值小于1,那么对应的线程就会被阻塞,直到前一个线程对信号量执行加1操作后才会继续执行。这样不论哪一个线程先执行,都能够保证同一时间只能有一个线程去操作账户,从而保证了共享资源的安全性。

示例–使用信号量实现线程之间的同步
#include "header.h"typedef struct
{int result;sem_t sem;
}OperArg;void* cal_func(void *arg)
{OperArg *s = (OperArg*)arg;int i = 1;for(; i<= 100; i++){s->result += i;}printf("[cal thread id:%lx] write %d to the structure\n",pthread_self(),s->result);sem_post(&s->sem);	//对信号量作加1操作,唤醒被此信号量阻塞的线程pthread_exit(NULL);
}void* get_func(void *arg)
{OperArg *s = (OperArg*)arg;sem_wait(&s->sem);			//由于将信号量初始化为0,所以当执行此线程时会被阻塞直到另外一个线程调用sem_post将信号量的值加1printf("[get thread id:%lx] read %d from the structure\n",pthread_self(),s->result);pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t cal, get;	OperArg arg;memset(&arg, 0, sizeof(arg));		//初始化结构体sem_init(&arg.sem, 0, 0);			//初始化信号量,信号量的值为0只用于当前进程的若干线程if((err = pthread_create(&cal, NULL, cal_func, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&get, NULL, get_func, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(cal, NULL);pthread_join(get, NULL);sem_destroy(&arg.sem);		//销毁信号量return 0;
}

image-20241009114122777

通过编译执行可以发现使用信号量可以用来实现线程之间的同步。在代码中要cal_func线程先计算出结果存放到result中才轮到get_func线程获取result的值。具体的做法就是阻塞get_func线程直到cal_func线程将结果计算出来,也就是说刚开始的信号量的值为0,那么当get_func线程调用sem_wait()去对信号量作减1操作的时候就会被阻塞直到cal_func线程调用sem_post()函数对信号量作加1操作才会执行,那么此时也就意味着已经将结果计算出来并放入到结构体中可以由get_func线程获取了,由此实现了线程之间的同步。

死锁

死锁的产生原因:

  1. 资源竞争
    • 多个线程需要竞争有限的资源(如锁、信号量、内存等),并且它们以不同的顺序请求这些资源,可能导致死锁,例如:线程A持有资源1并请求资源2,而线程B持有资源2并请求资源1,形成循环等待
  2. 持有并等待
    • 一个线程在已持有某个资源的同时请求其他的资源,这种情况很容易导致死锁,因为其他线程可能会占用这些请求的资源,形成等待状态。例如:线程A已经获得了资源X,并请求资源Y,而线程B已经获得了资源Y,并请求资源X。
  3. 不可剥夺性
    • 线程持有的资源不能被强制剥夺,只有当线程释放它们时,其他线程才能获取这些资源。这样的情况下,如果多个线程相互等待彼此持有的资源,就可能导致死锁。
  4. 循环等待
    • 一组线程形成一个等待环,其中每个线程都在等待下一个线程所持有的资源。这种条件是死锁的必要条件。例如:线程1等待线程2持有的资源,线程2等待线程3持有的资源,而线程3又在等待线程1持有的资源。

示例–死锁的产生

#include "header.h"typedef struct
{int value;pthread_mutex_t mutex;
}ResourceA;typedef struct
{int value;pthread_mutex_t mutex;
}ResourceB;typedef struct
{ResourceA *a;ResourceB *b;
}Resource;void* exec_func1(void *arg)
{Resource *s = (Resource*)arg;//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);	printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);pthread_exit(NULL);
}void* exec_func2(void *arg)
{Resource *s = (Resource*)arg;//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);sleep(1);printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->a->mutex);pthread_mutex_unlock(&s->b->mutex);pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2;ResourceA a;ResourceB b;Resource arg = {&a, &b};a.value = 100;b.value = 200;pthread_mutex_init(&a.mutex, NULL);pthread_mutex_init(&b.mutex, NULL);if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_mutex_destroy(&a.mutex);pthread_mutex_destroy(&b.mutex);return 0;
}

image-20241009171651310

通过编译执行可以发现线程陷入了死循环,分析其代码造成死循环的主要原因是:线程func1对资源A进行上锁后延时1s,此时会轮到线程func2执行,然后线程func2会对资源B进行上锁然后延时1s,当再次轮到线程func1运行的时候,线程func1尝试去获取资源B的锁,但是资源B的锁已经被func2所获取,所以此时线程func1就会被阻塞,而线程func2同理想要获取资源A的锁也会被阻塞,所以两个线程在已经持有一个锁的情况下想要获取对方的锁就造成了死锁。

解决死锁的办法:

  1. 按相同的次序锁定相应的共享资源
  2. 使用函数pthread_mutex_trylock(),当它首次上锁的时候,如果该互斥锁没有被别的线程锁定,则调用成功,锁会被获取。如果再次调用的时候,如果该互斥锁已经被其他的线程锁定,pthread_mutex_trylock()会立即返回,并不会阻塞。此时,函数返回的错误码是EBUSY,表示互斥锁正在被使用,当前线程无法获取互斥锁。

示例–使用相同次序锁定解决死锁

#include "header.h"typedef struct
{int value;pthread_mutex_t mutex;
}ResourceA;typedef struct
{int value;pthread_mutex_t mutex;
}ResourceB;typedef struct
{ResourceA *a;ResourceB *b;
}Resource;void* exec_func1(void *arg)
{Resource *s = (Resource*)arg;//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func1 thread id:%lx waiting for resourceB....\n",pthread_self());//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);	printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);pthread_exit(NULL);
}void* exec_func2(void *arg)
{Resource *s = (Resource*)arg;pthread_mutex_lock(&s->a->mutex);sleep(1);printf("func2 thread id:%lx waiting for ResourceB\n",pthread_self());pthread_mutex_lock(&s->b->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->b->mutex);pthread_mutex_unlock(&s->a->mutex);/*//对共享资源B进行上锁pthread_mutex_lock(&s->b->mutex);sleep(1);printf("func2 thread id:%lx waiting for resourceA....\n",pthread_self());//对共享资源A进行上锁pthread_mutex_lock(&s->a->mutex);printf("ResourceA value = %d\n",s->a->value);printf("ResourceB value = %d\n",s->b->value);pthread_mutex_unlock(&s->a->mutex);pthread_mutex_unlock(&s->b->mutex);
*/pthread_exit(NULL);
}int main(void)
{int err = -1;pthread_t func1, func2;ResourceA a;ResourceB b;Resource arg = {&a, &b};a.value = 100;b.value = 200;pthread_mutex_init(&a.mutex, NULL);pthread_mutex_init(&b.mutex, NULL);if((err = pthread_create(&func1, NULL, exec_func1, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}if((err = pthread_create(&func2, NULL, exec_func2, (void*)&arg)) != 0){perror("pthread_create error");exit(EXIT_FAILURE);}pthread_join(func1, NULL);pthread_join(func2, NULL);pthread_mutex_destroy(&a.mutex);pthread_mutex_destroy(&b.mutex);return 0;
}

image-20241009173516117

通过编译执行可以看出通过修改锁定的次序后线程没有再次进入到死锁状态了。分析代码:当线程func1开始执行的时候,先对资源A进行上锁,然后进入到睡眠状态轮到线程func2执行,此时线程func2想要获取互斥锁,但是互斥锁已经被线程func1获取并锁定,所以线程func2没有获取到互斥锁进入到阻塞状态,直到线程func1执行完释放锁以后线程func2才能够获取锁并执行。经过这个操作以后就能够实现避免死锁现象,并且保证线程的安全性。

线程状态转换

在这里插入图片描述

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

力扣HOT100合集

力扣HOT100 - 1. 两数之和 解题思路&#xff1a; 解法一&#xff1a;暴力 class Solution {public int[] twoSum(int[] nums, int target) {int n nums.length;for (int i 0; i < n; i)for (int j i 1; j < n; j) {if (target nums[i] nums[j])return new int[] …

操作系统-系统调用

应用程序调用printf(),会触发系统调用write() 1、概念 操作系统服务的编程接口&#xff0c;通常由高级语言编写&#xff08;C/C&#xff09;&#xff0c;程序访问通常是通过高层次的API接口而不是直接进行系统调用。 2、三种最常用的应用程序编程接口&#xff08;API&#xf…

Vue深入了解

Vue深入了解 MVVMv-model (双向数据绑定原理)异步更新keep-alive原理$nextTick原理computed 和 watch 的区别css-scoped虚拟DOMVuex && PiniaVue-router原理proxy 与 Object.defineProperty组件通信方式 MVVM <!DOCTYPE html> <html lang"en">&…

AD原理图编译出现Net XX has no driving source

提示无驱动电压源&#xff0c;这是因为你的芯片管脚设置了电气属性造成的。 两种解决AD中出现Net has no driving source警告的方法。 方法一&#xff1a;取消电气属性检测&#xff0c;但不推荐&#xff1b; 打开原理图编译项&#xff0c;将NET no driving source 修改为no …

PostgreSQL的学习心得和知识总结(一百五十三)|[performance]将 OR 子句转换为 ANY 表达式

目录结构 注&#xff1a;提前言明 本文借鉴了以下博主、书籍或网站的内容&#xff0c;其列表如下&#xff1a; 1、参考书籍&#xff1a;《PostgreSQL数据库内核分析》 2、参考书籍&#xff1a;《数据库事务处理的艺术&#xff1a;事务管理与并发控制》 3、PostgreSQL数据库仓库…

树控件QTreeWidget

树控件跟表格控件类似&#xff0c;也可以有多列&#xff0c;也可以只有1列&#xff0c;可以有多行&#xff0c;只不过每一行都是一个QTreeWidgetItem&#xff0c;每一行都是一个可以展开的树 常用属性和方法 显示和隐藏标题栏 树控件只有水平标题栏 //获取和设置标题栏的显…

PPT在线画SWOT分析图!这2个在线软件堪称办公必备!

swot分析ppt怎么做&#xff1f; swot分析是一个非常常用的战略分析框架&#xff0c;经常会在ppt中使用。想在ppt中绘制swot分析图&#xff0c;使用自带的形状工具可以制作出来&#xff0c;但绘制效率不够高&#xff0c;在需要大批量制作的场景下&#xff0c;会让人非常心累………

DepthB2R靶机打靶记录

一、靶机介绍 下载地址&#xff1a;https://download.vulnhub.com/depth/DepthB2R.ova 二、信息收集 根据靶机主页显示&#xff0c;确认靶机ip为192.168.242.132 端口扫描 nmap -p- -A 192.168.242.132 发现只开放了8080端口 用dirsearch扫个目录 apt-get update apt-get …

基于LORA的一主多从监测系统_0.96OLED

关联&#xff1a;0.96OLED hal硬件I2C LORA 在本项目中每个节点都使用oled来显示采集到的数据以及节点状态&#xff0c;OLED使用I2C接口与STM32连接&#xff0c;这个屏幕内部驱动IC为SSD1306&#xff0c;SSD1306作为从机地址为0x78 发送数据&#xff1a;起始…

【Linux】基本认知全套入门

目录 Linux简介 Linux发行版本 发行版选择建议 Centos-社区企业操作系统 Centos版本选择 Linux系统目录 Linux常用命令 SSH客户端 Linux文件操作命令 vim重要快捷键 应用下载与安装 netstat&#xff0c;ps与kill命令使用 Linux应用服务化 Linux用户与权限 Linu…

Telephony CarrierConfig配置

1、CarrierConfig配置介绍 CarrierConfig&#xff08;运营商配置&#xff09;&#xff0c;是Android为了针对不同运营商配置不同功能的配置文件&#xff0c;类似Modem的MBN配置&#xff0c;可以实现插入不同运营商卡&#xff0c;不同的功能实现或菜单显示等。 2、CarrierConfig…

力扣之1355.活动参与者

题目&#xff1a; Sql 测试用例&#xff1a; Create table If Not Exists Friends (id int, name varchar(30), activity varchar(30)); Create table If Not Exists Activities (id int, name varchar(30)); Truncate table Friends; insert into Friends (id, name, acti…

【数据结构与算法-高阶】并查集

【数据结构与算法-高阶】并查集 &#x1f955;个人主页&#xff1a;开敲&#x1f349; &#x1f525;所属专栏&#xff1a;数据结构与算法&#x1f345; &#x1f33c;文章目录&#x1f33c; 1. 并查集原理 2. 并查集实现 3. 并查集应用 1. 并查集原理 在一些应用问题中&…

charAt,chartCodeAt,codePointAt,fromCodePoint,fromCharCode

生僻字的length算2,有些空格是特殊空格,比如\u3000 u3000不是全角空格&#xff0c;u3000是表意字空格&#xff08;Ideographic Space&#xff09;&#xff0c;宽度和一个表意字&#xff08;汉字&#xff09;相同。它应当被当做汉字来处理。比如&#xff0c;在一些排版中&#x…

OpenSource - License 开源项目 TrueLicense

文章目录 官网集成Demo 官网 https://truelicense.namespace.global/ https://github.com/christian-schlichtherle/truelicense 集成Demo https://github.com/christian-schlichtherle/truelicense-maven-archetype https://github.com/zifangsky/LicenseDemo https://git…

map和set(c++)

前言 在前面我们在介绍二叉搜索树时我们分别实现了一个key结构和key-val结构&#xff0c;如果我们再进一步完善这棵树&#xff0c;将二叉搜索树升级为红黑树去存储key和key-val那么我们就可以得到我们今天要介绍的主角map和set。当然了标准库的实现还是有很多需要注意的地方&a…

植物大战僵尸修改器-MFC

创建项目 创建mfc应用 基于对话框 打开资源视图下的 IDD_MFCAPPLICTION2_DIALOG 限制对话框大小 将属性中Border的值改为对话框外框 删除对话框中原有的控件 属性-外观-Caption 设置对话框标题 工具箱中拖放一个按钮 修改按钮名称 将按钮ID改为IDC_COURSE 在MFCApplication2…

k8s微服务

一 、什么是微服务 用控制器来完成集群的工作负载&#xff0c;那么应用如何暴漏出去&#xff1f;需要通过微服务暴漏出去后才能被访问 Service是一组提供相同服务的Pod对外开放的接口。 借助Service&#xff0c;应用可以实现服务发现和负载均衡。 service默认只支持4层负载均…

QT安装成功后-在创建项目时,发现仅有项目名文件

&#xff08;1&#xff09;QT安装成功后&#xff0c;发现仅有项目名文件其他可编辑文件缺失 &#xff08;2&#xff09;点击文件名左上角的感叹号显示【No kits are enabled for this project. Enable】 小编在尝试多次后发现&#xff0c;可以通过以下方式解决&#xff1a;QT软…

接着上一篇stp 实验继续

理论看上一篇&#xff0c;我们直接实验 首先找出&#xff52;&#xff4f;&#xff4f;&#xff54; 桥 很明显 &#xff53;&#xff57;&#xff11; 为&#xff52;&#xff4f;&#xff4f;&#xff54; 桥&#xff0c;所谓&#xff53;&#xff57;&#xff11;  &a…