【C语言】多线程之条件竞争

news/2024/4/25 21:32:14/文章来源:https://blog.csdn.net/zhang2929259676/article/details/129205112

多线程(三)

  • 条件竞争
    • 并发程序引起的共享内存的问题
      • 死锁
    • 互斥锁机制
      • 生产者消费者模型
    • 信号量机制
  • 解决:

条件竞争

#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
void* Print(char* str){printf("%s ",str);
}int main(){pthread_t thread1,thread2;pthread_create(&thread1,NULL,(void*)&Print,"Hello");pthread_create(&thread2,NULL,(void*)&Print,"World");return 0;
}

编译运行后,发现没有预期输出。

原因:主线程的退出会导致创建的线程退出

使用pthread_create函数创建两个线程,两个线程创建后,并不影响主线程的执行,所以这里就存在三个线程的竞争关系。主线程执行return 0;先于另外两个线程的打印函数。所以看不见另外两个线程的输出。为了使return 0;语句在另外两个进程后执行,可以采用sleep()函数进行延迟,就可以得到输出了,这就是条件竞争。

在遇到条件竞争的问题中,采用sleep()函数进行延迟似乎也能解决问题。实则不然,弊端很明显:

  1. 不能判断延迟的时间长度。
  2. 使程序执行卡顿。

最适当的解决方法是采用锁机制。

并发程序引起的共享内存的问题

有两个进程,两个进程共享全局变量s。两个进程都执行一个计数功能的函数,直观地看过去,th1运行时s++要执行10000次,th2运行时s++也要执行10000次,似乎计算得到的最后s应该是20000。但实际上是这样的吗?

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
int s = 0;
void *func(void*args){int i;for(i = 0; i < 10000; i++){s++;}return NULL;
}
int main(){pthread_t th1;pthread_t th2;pthread_create(&th1, NULL, func, NULL);pthread_create(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("%s = %d\n", s);return 0;
}

编译运行后,发现输出并不是20000,而是12657。

运行了3次这个程序,每次的结果都不同。 用这个演示来表示一下,多线程之间是资源共享的。

s++ 是有三个步的,读取s,s+1,写入s。

在程序运行的某个时刻,th1携带myfunc执行s++,读取s,此时s=100,进行s+1, 与此同时th2也开始读取s,此时的s还是等于100, 这时th1,执行写入s=101,th2执行s++,写入s ,s=101. th2中的s 就会覆盖掉 th1中的s 。这样造成了结果的误差。

原因:当我们执行s++,底层发生的事件其实是:内存中读取s→将s+1→将s写入到内存。这不是一个原子化操作,当两个线程交错运行的时候,很容易发生结果的丢失。因此最后的结果肯定是要小于20000的。这种情况有种专有名词,叫race condition。

为了解决这个问题,我们可以加锁。

#include <pthread.h>
int s = 0;
pthread_mutex_t lock;	//锁的声明
void *func(void *args){int i;for(i = 0; i < 10000; i++){	//给临界区代码加锁实现原子化操作pthread_mutex_lock(&lock);s++;pthread_mutex_unlock(&lock);}return NULL;
}
int main(){pthread_t th1;pthread_t th2;//锁初始化pthread_mutex_init(&lock, NULL);pthread_create(&th1, NULL, func, NULL);pthread_createe(&th2, NULL, func, NULL);pthread_join(th1, NULL);pthread_join(th2, NULL);printf("s = %d\n, s);return 0;
}

改进后的代码如下,学过操作系统会很好理解,无非就是为了保证共享内存区(临界区)的原子化操作,我们可以在进这段代码之前加锁(pthread_mutex_lock),意味着其他线程看到这段内存被其他人占有的时候,就不去抢占,等这段内存被解锁(pthread_mutex_unlock)之后,它才有读写这段临界区的权利。

但其实这种方式的执行速度并不快,比如这段代码里,每个线程都要进行10000次加解锁的操作,它能解决内存读写冲突的问题,但是却牺牲了效率。
锁的作用是什么呢?

前面说过多线程是并发执行的,th1运行后 进行了加锁,th2这时候想要运行,就必须等待th1解锁之后才行。
(一个卫生间,多个人要用,第一个人进去之后,把门锁上了,后边的人就得排队等着,第一个方便完了,解锁开门出来,第二个人进去,继续锁门……)

锁 在提高程序的安全性的同时,也降低了程序的效率。

锁的使用方法

pthread_mutex_t lock; 声明一个锁

pthread_mutex_init(&lock,NULL); 对声明的锁进行初始化

pthread_mutex_lock(&lock); //上锁 此时其他线程就开始等待

pthread_mutex_lock(&unlock); //解锁 其他线程可以使用资源了

死锁

拿上边举例,th1运行后,th2会等待th1解锁,才能运行,如果程序出现错误中断了,th1没有执行完,重新启动后,th1又重新执行,这时候th2排在th1前边,需要等待th1的解锁 ,而th1又在等待th2结束。 这样就造成了相互等待的情况,这个就是死锁。

互斥锁机制

通过访问时对共享资源加锁的方法,防止多个线程同时访问共享资源。锁有两种状态:未上锁和已上锁。在访问共享资源时,进行上锁,在访问结束后,进行解锁。若在访问时,共享资源已被其它线程锁住了,则进入堵塞状态等待该线程释放锁再继续下一步的执行。这种锁我们称为互斥锁。

互斥锁相关函数介绍:

1、pthread_mutex_init :初始化一个互斥锁。

函数原型:int pthread_mutex_init(pthread_mutex_tmutex,constpthread_mutexattr_tattr);

2、pthread_mutex_lock:若所访问的资源未上锁,则进行lock,否则进入堵塞状态。

函数原型:intpthread_mutex_lock(pthread_mutex_t*mutex);

3、pthread_mutex_unlock:对互斥锁进行解锁。

函数原型:intpthread_mutex_unlock(pthread_mutex_t*mutex);

4、pthread_mutex_destroy:销毁一个互斥锁。

函数原型:intpthread_mutex_destroy(pthread_mutex_t*mutex);

生产者消费者模型

生产者和消费者在同一时间段内共用同一个存储空间,生产者往存储空间中生成产品,消费者从存储空间中取走产品。当存储空间为空时,消费者阻塞;当存储空间满时,生产者阻塞。(下面代码中存储空间为1)

#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>int buf = 0;pthread_mutex_t mut;
void producer(){while(1){pthread_mutex_lock(&mut);if(buf == 0){buf = 1;printf("produced an item.\n");sleep(1);}pthread_mutex_unlock(&mut);}
}
void consumer(){while(1){pthread_mutex_lock(&mut);if(buf == 1){buf = 0;printf("consumed an item.\n");sleep(1);}pthread_mutex_unlock(&mut);}
}int main(void){pthread_t thread1,thread2;pthread_mutex_init(&mut,NULL);pthread_create(&thread1,NULL,&producer,NULL);consumer(&buf);pthread_mutex_destroy(&mut);return 0;
}

从执行结果可以看出,运行顺序井然有序。生产后必是消费,消费完后必是生产。由于互斥锁机制的存在,生产者和消费者不会同时对共享资源进行访问。

信号量机制

上面了解到的互斥锁有两种状态:资源为0和1的状态。当我们所拥有的资源大于1时,可以采用信号量机制。在信号量机制中,我们有n个资源(n>0)。在访问资源时,若n>=1,则可以访问,同时信号量-1,否则堵塞等待直到n>=1。其实互斥锁可以看出信号量的一种特殊情况(n=1)。

信号量相关函数的介绍:
头文件:semaphore.h
1、sem_init函数:初始化一个信号量。
函数原型:int sem_init(sem_t* sem, int pshared, unsigned int value);
参数:sem:指定了要初始化的信号量的地址;pshared:如果其值为0,就表示信号量是当前进程的局部信号量,否则信号量就可以在多个进程间共享;value:指定了信号量的初始值;返回值:成功=>0 , 失败=> -12、 sem_post函数:信号量的值加1,如果加1后值大于0:等待信号量的值变为大于0的进程或线程被唤醒。
函数原型:int sem_post(sem_t* sem);
返回值:成功=>0 , 失败=> -13、sem_wait函数:信号量的减1操作。如果当前信号量的值大于0,则可继续执行。如果当前信号量的值等于0,则会堵塞,直到信号量的值大于0.
函数原型:int sem_wait(sem_t* sem);
返回值:成功=>0 , 失败=> -14、sem_destroy函数:销毁一个信号量。
函数原型:int sem_destroy(sem_t* sem);
返回值:成功=>0 , 失败=> -15、sem_getvalue函数:获取信号量中的值。
函数原型:int sem_getvalue(sem_t* sem, int* sval);
获取信号量的值,并放在&sval上。
#include<stdio.h>
#include<stdlib.h>
#include<pthread.h>
#include<semaphore.h>
#include<unistd.h>sem_t npro; //还可以生产多少
sem_t ncon; //还可以消费多少void* producer(void* arg){while(1){int num;sem_wait(&npro); //先判断是否可以生产sem_post(&ncon); //生产一个,可消费数+1sem_getvalue(&ncon,&num);printf("produce one,now have %d items.\n",num);sleep(0.7);}
}void consumer(void* arg){while(1){int num;sem_wait(&ncon); //判断是否可以消费sem_post(&npro); //消费一个,可生产数+1sem_getvalue(&ncon,&num);printf("consume one,now have %d items.\n",num);sleep(1);}
}int main(void){pthread_t thread1,thread2;//init semaphoresem_init(&npro,0,5); //设最大容量为5sem_init(&ncon,0,0);pthread_create(&thread1,NULL,&producer,NULL);consumer(NULL);return 0;
}

同样也可以解决条件竞争问题,而且使用范围更广了。

解决:

  • pthread_join()函数
  • 互斥锁机制
  • 信号量机制

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

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

相关文章

【PyQt5图形界面编程(2)】:创建工程

创建工程 一、创建工程二、开始开发1、运行Qt5Designer,创建QT窗口2、运行pyUIC,转换xx.ui成xx.py3、main.py中引用xx.py中的类4、打包main.py成main.exe来发布5、执行终端报警处理方法三、其他(如果涉及)1、配置环境变量一、创建工程 采用虚拟环境来创建工程 相关的paka…

MyBatis学习笔记(五) —— MyBatis获取参数值的两种方式

5、MyBatis获取参数值的两种方式 MyBatis获取参数值的两种方式&#xff1a;${} 和 #{} ${} 的本质就是字符串拼接&#xff0c; #{} 的本质就是占位符赋值 ${} 使用字符串拼接的方式拼接sql&#xff0c;若为字符串类型或日期类型的字段进行赋值时&#xff0c;需要手动加单引号&a…

SpringBoot整合阿里云OSS文件上传、下载、查看、删除

SpringBoot整合阿里云OSS文件上传、下载、查看、删除1、开发准备1.1 前置知识1.2 环境参数1.3 你能学到什么2. 使用阿里云OSS2.1 创建Bucket2.2 管理文件2.3 阿里云OSS文档3. 项目初始化3.1 创建SpringBoot项目3.2 Maven依赖3.3 安装lombok插件4. 后端服务编写4.1 阿里云OSS配置…

4面美团软件测试工程师,却忽略了这一点,直接让我前功尽弃

说一下我面试别人时候的思路 反过来理解&#xff0c;就是面试时候应该注意哪些东西&#xff1b;用加粗部分标注了 一般面试分为这么几个部分&#xff1a; 一、自我介绍 这部分一般人喜欢讲很多&#xff0c;其实没必要。大约5分钟内说清楚自己的职业经历&#xff0c;自己的核…

自动驾驶仿真测试介绍

作者 | 楼泽如 上海控安可信软件创新研究院研发工程师 来源 | 鉴源实验室 01 引 言 自动驾驶汽车的兴起&#xff0c;正在重新定义汽车行业。随着自动驾驶技术的发展&#xff0c;自动驾驶汽车将会大大提升交通安全、减少事故发生、减少交通拥堵、提高公路容量等等&#xff0…

Java学习笔记 --- 正则表达式

一、体验正则表达式 package com.javase.regexp;import java.util.regex.Matcher; import java.util.regex.Pattern;/*** 体验正则表达式&#xff0c;给文本处理带来哪些便利*/ public class Regexp_ {public static void main(String[] args) {//假设&#xff0c;编写了爬虫&…

Linux——UDP协议与相关套接字编程

一.概念在网络通信中&#xff0c;传输层中最常用的通信协议有两个&#xff1a;TCP协议与UDP协议。这两种协议虽然都可以用于网络通信&#xff0c;但是通信方式不同决定了应用场景的不同。与TCP协议相比&#xff0c;UDP协议最具特色的不同点有两个&#xff1a;无连接与面向数据报…

码住!新手容易上手的5个tiktok数据分析网站

当下短视频已经称霸了各大内容平台&#xff0c;越来越多的创作者进入到短视频赛道&#xff0c;为了更好地运营自己的内容平台&#xff0c;数据分析是必不可少的。很多人都入局了tiktok&#xff0c;对于商家或者博主红人来说&#xff0c;这是比较新平台&#xff0c;希望能在这个…

python库streamlit学习笔记

什么是streamlit&#xff1f; Streamlit是一个免费的开源框架&#xff0c;用于快速构建和共享漂亮的机器学习和数据科学Web应用程序。它是一个基于Python的库&#xff0c;专为机器学习工程师设计。数据科学家或机器学习工程师不是网络开发人员&#xff0c;他们对花几周时间学习…

Vue-cli脚手架在做些什么(源码角度分析)

什么是Vue脚手架&#xff1f;在学习初期&#xff0c;我们的项目往往需要借助webpack、vite等打包工具配置Vue的开发环境&#xff0c;但是在真实开发中我们不可能每个项目从头来完成所有的webpack配置&#xff0c;这样显得开发的效率会大大的降低&#xff1b;所有的真实开发中&a…

实现基于国密SM3的密钥派生(KDF)功能

实现基于国密SM3的密钥派生&#xff08;KDF&#xff09;前言KDF 标准基于SM3的kdf实现前言 密钥派生函数&#xff08;KDF&#xff09;&#xff1a;密钥派生函数是指从一个共享的秘密比特串中派生密钥数据&#xff0c;在密钥协商过程中&#xff0c;密钥派生函数作用在密钥交换所…

【一看就会】实现仿京东移动端页面滚动条布局

简单粗暴直接上代码&#xff1a; <!DOCTYPE html> <html lang"en"> <head> <meta charset"UTF-8"> <meta http-equiv"X-UA-Compatible" content"IEedge"> <meta name"viewport" content&q…

中移链结合CA证书实现节点准入控制

01背景介绍BSN开放联盟链&#xff08;BSN Open Permissioned Blockchain&#xff0c; 简称OPB&#xff09;包括多条基于公有链框架和联盟链框架搭建的公用链&#xff0c;开发者可以选择适合应用业务需求的开放联盟链部署和运行智能合约和分布式应用&#xff0c;每条开放联盟链各…

码匠 × OpenAI :快速生成 SQL 语句,提升开发效率!

目录 使用 OpenAI 生成 SQL 码匠连接与集成 OpenAI 总结 关于码匠 在码匠中&#xff0c;编写 SQL 语句&#xff0c;并结合码匠一系列开箱即用的组件实现复杂的业务逻辑&#xff0c;是很常见的应用开发场景。然而&#xff0c;不同的数据库在 SQL 增删改查操作语法、类型字段和…

Java知识复习(二)Java集合

1、List、Set和Map的区别 List&#xff1a;存储的顺序是有序的、可重复的Set&#xff1a;存储的顺序是无序的、不可重复的Map&#xff1a;使用键值对存储&#xff0c;Key和Value都是无序的&#xff0c;其中Key不可重复&#xff0c;而Value可重复 2、ArrayList和LinkedList的区…

JavaEE简单实例——MyBatis的一对一映射的嵌套查询的简单介绍和基础配置

简单介绍&#xff1a; 在前一章我们介绍了关于MyBatis的多表查询的时候的对应关系&#xff0c;其中有三种对应关系&#xff0c;分别是一对一&#xff0c;一对多&#xff0c;多对多的关系。如果忘记了这三种方式的对应形式可以去前面看看&#xff0c;一定要记住这三种映射关系的…

项目重构,从零开始搭建一套新的后台管理系统

背景 应公司发展需求&#xff0c;我决定重构公司的后台管理系统&#xff0c;从提出需求建议到现在的实施&#xff0c;期间花了将近半个月的时间&#xff0c;决定把这些都记录下来。 之前的后台管理系统实在是为了实现功能而实现的&#xff0c;没有考虑到后期的扩展性&#xf…

逆风翻盘拿下感知实习offer,机会总是留给有准备的人

个人背景211本&#xff0c;985硕&#xff0c;本科是计算机科学与技术专业&#xff0c;研究生是自学计算机视觉方向&#xff0c;本科主要做C和python程序设计开发&#xff0c;java安卓开发&#xff0c;研究生主要做目标检测&#xff0c;现在在入门目标跟踪和3d目标检测。无论文&…

并发编程学习篇从0-1合集

一、synchronized 一、原子性、有序性、可见性 1.1 原子性 数据库的事务&#xff1a;ACID A&#xff1a;原子性-事务是一个最小的执行的单位&#xff0c;一次事务的多次操作要么都成功&#xff0c;要么都失败。 并发编程的原子性&#xff1a;一个或多个指令在CPU执行过程中…

用Python获取弹幕的两种方式(一种简单但量少,另一量大管饱)

前言 弹幕可以给观众一种“实时互动”的错觉&#xff0c;虽然不同弹幕的发送时间有所区别&#xff0c;但是其只会在视频中特定的一个时间点出现&#xff0c;因此在相同时刻发送的弹幕基本上也具有相同的主题&#xff0c;在参与评论时就会有与其他观众同时评论的错觉。 在国内…