【操作系统】Linux编程 - 多线程的创建和使用 II (临界区 、互斥量、信号量的使用)

news2024/10/6 12:27:33

临界区的概念 

        之前的实例中我们只尝试创建了1个线程来处理任务,接下来让我们来尝试创建多个线程。

        不过,还是得先拓展一个概念——“临界区”

        临界区指的是一个访问共用资源(例如:共用设备或是共用存储器)的程序片段,而这些共用资源又无法同时被多个线程访问的特性。有线程进入临界区段时,其他线程或是进程必须等待(例如:bounded waiting 等待法),有一些同步的机制必须在临界区段的进入点与离开点实现,以确保这些共用资源是被互斥获得使用,例如:semaphore。

        多个线程同时执行时很容易产生问题,这个问题集中在对共用资源的访问上。

        根据函数在执行时是否会导致在临界区发生问题,可将函数的类型分为两类:

  • 线程安全函数 (Thread-safe function)
  • 非线程安全函数 (Thread-unsafe function)

        线程安全函数在被多个线程同时调用时不会引发问题,而非线程安全函数在被多个线程调用时则会发生问题。

拓展:

        无论是Linux还是Windwos,我们都无需去区分线程安全函数和非线程安全函数,因为这在设计非线程安全函数的同时,开发者们也设计了具有相同功能的线程安全函数。

        线程安全函数的名称一般是在函数添加后缀_r ,但在编程中如果我们全以这种方式来书写函数表达式,那么将会变得十分麻烦,为此我们可以通过在声明头文件前定义_REENTRANT宏。

        如果追求更加快捷的代码编写体验,可以在编译键入参数时加入- D_REENTRANT,而不在编写代码时去引用_REENTRANT宏。

模拟多线程任务

        接下来,让我们模拟出一个场景把这个问题体现出来,下列为示例代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    return 0;
}

void *thread_inc(void *arg)
{
    for (int i = 0; i < 100000; i++)
        num += 1;
    return NULL;
}

void *thread_des(void *arg)
{
    int i;
    for (int i = 0; i < 100000; i++)
        num -= 1;
    return NULL;
}

运行结果:

        很明显结果并不是实际想要的“0”值 ,并且该输出值是随时变化的。

        那么是什么原因导致这样不符合实际的值的出现呢?

        在这里列举一种情形:

        当线程A发起对变量λ=98的访问时,线程B也发起了访问,那么此时线程A、B都拿到了λ=98的数值。线程对该数值进行 +1 计算后,得出了99,并向该资源变量发起更新请求,但此时线程B也做同样的操作,并且是以之前同样拿到的数值λ=98为基础,那么最终的结果便是A算出了λ=99,B算的也是λ=99,最后更新的数值也是99,而实际应是100。

        总结来讲,造成这类问题的原因在于相同时间内对同一资源的访问、处理出现了“时差”,导致了最终结果与实际偏离。

        明白了原因,这个问题就很好解决了,那就是要把正在同时访问的资源读、写权限做一个限制,将线程同步起来。

线程同步

        对于线程的同步,需要依靠“互斥量”和“信号量”这2种概念定义。

互斥量

        互斥量用以限制多个线程同时访问,主要解决线程同步访问的问题,是一种“锁”的机制。

        而互斥量在pthread.h库中也有专门的函数,用以创建和销毁,让我们来看看他的函数结构:

#include <pthread.h>

int pthread_mutex_init(pthread_mutex_t * mutex , const pthread_mutexattr_t * attr);
int pthread_mutex_destroy(pthread_mutex_t * mutex);

//成功时返回 0 ,失败时返回其他值。

/* 参数定义
 
    mutex: 创建互斥量时传递保存互斥量的变量地址值,销毁时传递需要销毁的互斥量地址值。
    attr:  传递即将创建的互斥量属性,没有需要指定的属性时可以传递NULL。

        另外,如果不需要配置特定的互斥量属性,可以通过使用PTHREAD_MUTEX_INITIALIZER宏来进行初始化,示例如下:

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

        不过,最好还是使用pthread_mutex_init函数来初始化,因为宏在调试时很难定位报错点,同时pthread_mutex_init对互斥量属性的设置也更直观可控一点。

互斥量锁住和解锁

        上面所说到的两个函数只用于创建和销毁,最关键的还是上锁解锁这两个操作函数,他们的结构如下:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t * mutex);
int pthread_mutex_unlock(pthread_mutex_t * mutex);

// 成功时返回 0 ,失败时返回其他值 。

        进入临界区前需调用的函数是pthread_mutex_lock,若调用该函数时发现有其他线程已进入临界区,那么此时pthread_mutex_lock函数不会返回值,除非直到里面的线程调用pthreaed_mutex_unlock函数退出临界区后。

        一般临界区的结构设计如下:

pthread_mutex_lock(&mutex);
//临界区的开始
//..........
//..........
//临界区的结束
pthread_mutex_unlock(&mutex);

        特别注意,pthread_mutex_unlock()pthread_mutex_lock()一般是成对的关系,如果线程退出临界区后没有对锁进行释放,那么其他等待进入临界区的线程将无法摆脱阻塞态,最终成为“死锁”状态。

        接下来让我们尝试一下用互斥量来解决之前出现的问题吧

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>

#define THREAD_NUM 100

void *thread_inc(void *arg);
void *thread_des(void *arg);

long num = 0;
pthread_mutex_t mutex;

int main(int argc, char *argv[])
{
    pthread_t thread_id[THREAD_NUM];
    int i;

    pthread_mutex_init(&mutex, NULL);

    for (i = 0; i < THREAD_NUM; i++)
    {
        if (i % 2)
            pthread_create(&(thread_id[i]), NULL, thread_inc, NULL);
        else
            pthread_create(&(thread_id[i]), NULL, thread_des, NULL);
    }

    for (i = 0; i < THREAD_NUM; i++)
        pthread_join(thread_id[i], NULL);

    printf("result: %ld \n", num);
    pthread_mutex_destroy(&mutex);
    return 0;
}

void *thread_inc(void *arg)
{
    pthread_mutex_lock(&mutex);
    // ↓临界区代码执行块
    for (int i = 0; i < 100000; i++)
        num += 1;
    // ↑临界区代码执行块
    pthread_mutex_unlock(&mutex);
    return NULL;
}
void *thread_des(void *arg)
{
    pthread_mutex_lock(&mutex);
    for (int i = 0; i < 100000; i++)
    {
        // ↓临界区代码执行块
        num -= 1;
        // ↑临界区代码执行块
    }
    pthread_mutex_unlock(&mutex);
    return NULL;
}

运行结果:

        结果终于正确了~

        需要特别注意的是,大家在设计锁的区域时一定要仔细考虑边界,确认出恰好需要“锁住”的那一个代码执行点和恰好可以结束的“释放”点,这样可以避免频繁调用“锁”和“解锁”操作,进而提高操作系统对于代码的执行效率。

信号量

        信号量与互斥量相似,也是一种实现线程同步的方法。一般信号量用二进制0和1来表示,因此也称这种信号量为“二进制信号量”。

        下面是信号量的创建及销毁方法:

#include <semaphore.h>
int sem_init(sem_t * sem , int pshared, unsigned int value);
int sem_destroy(sem_ t * sem);

//成功时返回0,失败时返回其他值

/* 参数含义
    
    sem: 创建信号量时传递保存信号量的变量地址值,销毁时传递需要销毁的信号量变量地址值。
    pshared: 传递0时,创建只允许1个进程内部使用的信号量。传递其他值时,创建可由多个进程共享的信号量。
    value: 指定新创建的信号量初始值。
*/

        与互斥量一样,有“锁”和“解锁”函数

#include <semaphore.h>
int sem_post(sem_ t * sem);
int sem_wait(sem_t * sem);

//成功时返回0,失败时返回其他值。

/* 参数含义

    sem: 传递保存信号量读取值的变量地址值,传递给sem_post时信号量增1,传递给sem_wait信号量减1。 

*/

        调用sem_init函数时,操作系统会创建信号量对象,并初始化好信号量值。在调用sem_post函数时该值+1,调用sem_wait函数时该值-1

        当线程调用sem_wait函数使信号量的值为0时,该线程将进入阻塞状态,若此时有其他线程调用sem_post函数,那么之前阻塞的线程将可以脱离阻塞态并进入到临界区。

        信号量的临界区结构一般如下(假设信号量初始值为1):

sem_wait(&sem); //进入到临界区后信号量为0
// 临界区的开始
// ..........
// ..........
// 临界区的结束
sem_post(&sem); // 信号量变为1

        信号量一般用以解决线程任务中具有强顺序的同步问题

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

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

相关文章

哈工大计算机网络课程网络层协议详解之:Internet路由BGP协议详解

哈工大计算机网络课程网络层协议详解之&#xff1a;BGP协议详解 在之前的网络层协议中&#xff0c;我们介绍了Internet网络两个自治系统内的路由协议&#xff1a;RIP协议和OSPF协议。这两个协议应该来说是自治系统内协议的两个代表性协议&#xff0c;前一个基于距离向量路由算…

优化内存利用:深入了解垃圾回收算法与回收器

&#x1f52d; 大家好&#xff0c;我是 vnjohn&#xff0c;在互联网企业担任 Java 开发&#xff0c;CSDN 优质创作者 &#x1f4d6; 推荐专栏&#xff1a;Spring、MySQL、Nacos、Java&#xff0c;后续其他专栏会持续优化更新迭代 &#x1f332;文章所在专栏&#xff1a;JVM &am…

机器学习面试题- 特征工程

目录标题 1、为什么要对特征做归一化2、对特征归一化的方法2.1 线性函数归一化2.2 零均值归一化 3、对数据预处理时&#xff0c;如何处理类别型特征3.1 序号编码3.2 独热编码3.3 二进制编码 4、什么是组合特征&#xff1f;如何处理高维组合特征&#xff1f;5、怎样有效地找到组…

​python接口自动化(十)--post请求四种传送正文方式(详解)​

简介 post请求我在之前的文章已经讲过一部分了&#xff0c;主要是发送一些较长的数据&#xff0c;还有就是数据比较安全等。我们要知道post请求四种传送正文方式首先需要先了解一下常见的四种编码方式&#xff1a; HTTP 协议规定 POST 提交的数据必须放在消息主体&#xff08;…

SpringBoot处理全局异常详解(全面详细+Gitee源码)

前言&#xff1a;在日常的开发工作中&#xff0c;项目在运行过程中多多少少是避免不了报错的&#xff0c;对于报错信息肯定不可以把全部信息都抛给客户端去显示&#xff0c;这里就需要我们对常见的七种异常情况统一进行处理&#xff0c;让整个项目更加优雅。 目录 一、基本介绍…

AMEYA360:航顺芯片产品有哪些 航顺家族介绍

经济型 HK32M050 家族 采用ARM Cotex-M0内核&#xff0c;最新工艺标准&#xff0c;最高48M主频&#xff0c;内置16K FALSH&#xff0c;4K SRAM&#xff0c;支持DMA&#xff0c;内置4个模拟比较器&#xff0c;2路运放&#xff08;PGA&#xff09;&#xff0c;支持多种通讯包括2个…

二叉树 — 给定二叉树中某个节点,返回该节点的后继节点

后继节点定义&#xff1a; 二叉树以中序的方式进行遍历打印&#xff0c;节点X的下一个节点&#xff0c;就是X的后继节点。 假设二叉树如下图所示&#xff1a;则中序遍历的后打印出来的就是 4 -> 2 -> 5 -> 1 -> 6 -> 3 -> 7。如果X 3&#xff0c;则X的后继节…

Docker网络模型以及容器网络初探(一)

〇、前言 安装Docker时&#xff0c;它会自动创建三个网络&#xff0c;默认bridge网桥&#xff08;创建容器默认连接到此网络&#xff09;、 none 、host。各个方式有各自的特点&#xff0c;它们有着特定的差距&#xff0c;比如网络性能等&#xff0c;一般按照实际应用方式手动…

大数据之数据采集项目总结——hadoop,hive,openresty,frcp,nginx,flume

1、前期准备 2、数据收集 1、开启openresty&#xff0c;nginx和frcp内网穿透 2、编辑并启动定时器 3、查看是否收集到了数据 数据收集阶段结束&#xff0c;进入下一个阶段 2、将收集到的切分好的数据上传到hdfs 使用的工具&#xff1a;flume flume像一个管道一样&#xff0c…

三十九、动态规划——线性DP问题-例题题解

线性DP问题的例题状态划分 一、问题&#xff1a;数字三角形1、题目内容2、状态划分1&#xff09;状态编号 f[i][j]2&#xff09;状态划分 3、题解 二、最长上升子序列1、题目内容2、状态划分1&#xff09;状态编号 f[i]2&#xff09;状态划分 3、题解 三、最长公共子序列1、题目…

【二维偏序+双指针】ABC245 E

E - Wrapping Chocolate (atcoder.jp) 题意&#xff1a; 思路&#xff1a; 因为两个数组都是无序的&#xff0c;因此可以考虑给这两个数组都排个序 将物品和盒子都按照两个维度去排序 我们可以先去枚举物品&#xff0c;然后去选对应的盒子 在选盒子的过程中&#xff0c;注…

【王道·操作系统】第四章 文件管理(下)

一、文件系统 1.1 文件系统的层次结构 用户需要通过操作系统提供的接口发出上述请求——用户接口由于用户提供的是文件的存放路径&#xff0c;因此需要操作系统一层一层地查找目录&#xff0c;找到对应的目录项——文件目录系统不同的用户对文件有不同的操作权限&#xff0c;因…

c++读取字符串字符时出错

这是我做的一个c爬虫程序但是在抓取网页的时候string类型传递出现了问题 以下是图片代码 url的值是 "http://desk.zol.com.cn/" 我不知道为什么数据传递会出问题 请大佬指教

Java 串口通信(RS232/485)

Java 串口通信&#xff08;RS232/485&#xff09; 一.串口通信页面二.串口服务实现1.Java 串口通信配置1.扩展包和依赖库2.Pom配置 2.启动类3.工具包类1.Common2.Crc16Modbus3.SerialUtil 4.WebSocket 配置1.启动配置2.监听配置 5.UI交互类1.串口配置对象2.串口信息获取接口3.R…

HOT39-对称二叉树

leetcode原题链接&#xff1a;对称二叉树 题目描述 给你一个二叉树的根节点 root &#xff0c; 检查它是否轴对称。 示例 1&#xff1a; 输入&#xff1a;root [1,2,2,3,4,4,3] 输出&#xff1a;true示例 2&#xff1a; 输入&#xff1a;root [1,2,2,null,3,null,3] 输出&a…

JVM03-优化垃圾回收

JVM的内存区域中&#xff0c;程序计数器、虚拟机栈和本地方法栈这3个区域是线程私有的&#xff0c;随着线程的创建而创建&#xff0c;销毁而销毁&#xff1b;栈中的栈帧随着方法的进入和退出进行入栈和出栈操作&#xff0c;每个栈帧中分配多少内存基本是在类结构确定下来的时候…

消息中间件面试题详解

RabbitMQ 如何保证消息不丢失 消息的重复消费问题如何解决 rabbitmq中死信交换机&#xff08;RabbitMQ延迟队列有了解吗&#xff09; 延迟队列&#xff1a;进入队列的消息会被延迟消费的队列 场景&#xff1a;超时订单&#xff0c;限时优惠&#xff0c;定时发布 延迟队列 …

【Linux】-第一个小程序(进度条)

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树 &#x1f389;作者宣言&#xff1a;认真写好每一篇博客 &#x1f38a;作者gitee:gitee &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作…

Activiti modoler 整合后报错 TypeError: Cannot read property ‘namespace‘ of undefined

之前在Demo整合过没问题&#xff0c;结果好不容易整合到现在的项目&#xff0c;结果出现成这个鬼样子……问题找了好久&#xff0c;一直以为是SpringSecurity请求限制没放开&#xff0c;所以找SpringSecurity的debug日志&#xff0c;浏览器请求有没有404、500、502等&#xff0…

将OpenAI和ChatGPT模型与LearnDash线上学习平台结合使用

人工智能革命来了&#xff01;&#xff08;以尽可能最好的方式。&#xff09;了解如何使用 Uncanny Automator 通过 OpenAI 和 ChatGPT 模型为您的线上学习和LearnDash LMS提供动力。 当人们听到“人工智能”这个词时&#xff0c;他们往往会想到流氓机器人、无政府状态的机器人…