详解Linux多线程中锁、条件变量、信号量

news2024/10/6 1:43:48

一文读懂Linux多线程中互斥锁、读写锁、自旋锁、条件变量、信号量

Hello、Hello大家好,我是木荣,今天我们继续来聊一聊Linux中多线程编程中的重要知识点,详细谈谈多线程中同步和互斥机制。

同步和互斥

  • 互斥:多线程中互斥是指多个线程访问同一资源时同时只允许一个线程对其进行访问,具有唯一性和排它性。但互斥无法限制访问者对资源的访问顺序,即访问是无序的;
  • 同步:多线程同步是指在互斥的基础上(大多数情况),通过其它机制实现访问者对资源的有序访问。在大多数情况下,同步已经实现了互斥,特别是所有写入资源的情况必定是互斥的。少数情况是指可以允许多个访问者同时访问资源。

互斥锁

在多任务操作系统中,同时运行的多个任务可能都需要使用同一种资源。为了同一时刻只允许一个任务访问资源,需要用互斥锁对资源进行保护。互斥锁是一种简单的加锁的方法来控制对共享资源的访问,互斥锁只有两种状态,即上锁( lock )和解锁( unlock )。

互斥锁操作基本流程

  1. 访问共享资源前,对互斥锁进行加锁
  2. 完成加锁后访问共享资源
  3. 对共享资源完成访问后,对互斥锁进行解锁

对互斥锁进行加锁后,任何其他试图再次对互斥锁加锁的线程将会被阻塞,直到锁被释放

互斥锁特性

  • 原子性:互斥锁是一个原子操作,操作系统保证如果一个线程锁定了一个互斥锁,那么其他线程在同一时间不会成功锁定这个互斥锁
  • 唯一性:如果一个线程锁定了一个互斥锁,在它解除锁之前,其他线程不可以锁定这个互斥锁
  • 非忙等待:如果一个线程已经锁定了一个互斥锁,第二个线程又试图去锁定这个互斥锁,则第二个线程将被挂起且不占用任何CPU资源,直到第一个线程解除对这个互斥锁的锁定为止,第二个线程则被唤醒并继续执行,同时锁定这个互斥锁

示例

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

 char *pTestBuf = nullptr; // 全局变量

 /* 定义互斥锁 */
pthread_mutex_t mutex;

void *ThrTestMutex(void *p)
{
    pthread_mutex_lock(&mutex);     // 加锁
    {
        pTestBuf = (char*)p;
        sleep(1);
    }
    pthread_mutex_unlock(&mutex);   // 解锁
}

int main()
{   
    /* 初始化互斥量, 默认属性 */
    pthread_mutex_init(&mutex, NULL);

    /* 创建两个线程对共享资源访问 */
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, ThrTestMutex, (void *)"Thread1");
    pthread_create(&tid2, NULL, ThrTestMutex, (void *)"Thread2"); 

    /* 等待线程结束 */
    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL); 

    /* 销毁互斥锁 */
    pthread_mutex_destroy(&mutex);  

    return 0;
}

读写锁

  • 读写锁允许更高的并行性,也叫共享互斥锁。互斥量要么是加锁状态,要么就是解锁状态,而且一次只有一个线程可以对其加锁。读写锁可以有3种状态:读模式下加锁状态、写模式加锁状态、不加锁状态
    一次只有一个线程可以占有写模式的读写锁,但是多个线程可以同时占有读模式的读写锁,即允许多个线程读但只允许一个线程写。
  • 当读操作较多,写操作较少时,可用读写锁提高线程读并发性

读写锁特性

  1. 如果有线程读数据,则允许其它线程执行读操作,但不允许写操作
  2. 如果有线程写数据,则其它线程都不允许读、写操作
  3. 如果某线程申请了读锁,其它线程可以再申请读锁,但不能申请写锁
  4. 如果某线程申请了写锁,其它线程不能申请读锁,也不能申请写锁
  5. 读写锁适合于对数据的读次数比写次数多得多的情况

读写锁创建和销毁

    #include <pthread.h>
    int phtread_rwlock_init(pthread_rwlock_t *restrict rwlock, const pthread_rwlockattr_t *restrict attr);
    int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
  • 参数:rwlock:读写锁,attr:读写锁属性
  • 返回值:成功返回0,出错返回错误码

读写锁加锁解锁

    #include <pthread.h>
    /** 加读锁 */
    int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
    /** 加写锁 */
    int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
    /** 释放锁 */
    int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);
  • 参数:rwlock:读写锁
  • 返回值:成功返回 0;出错,返回错误码

示例

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

/* 定义读写锁 */
pthread_rwlock_t rwlock;

/* 定义共享资源变量 */
int g_nNum = 0;

/* 读操作 其他线程允许读操作 不允许写操作 */
void *fun1(void *arg)  
{  
    while(1)  
    {  
        pthread_rwlock_rdlock(&rwlock);  
        {
            printf("read thread 1 == %d\n", g_nNum);
        }      
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
}  

/* 读操作,其他线程允许读操作,不允许写操作 */
void *fun2(void *arg)
{    
    while(1)
    {
        pthread_rwlock_rdlock(&rwlock);  
        {
            printf("read thread 2 == %d\n", g_nNum);
        }      
        pthread_rwlock_unlock(&rwlock);

        sleep(1);
    }
} 

/* 写操作,其它线程都不允许读或写操作 */
void *fun3(void *arg)
{    
    while(1)
    {
        pthread_rwlock_wrlock(&rwlock);
        {
            g_nNum++;        
            printf("write thread 1\n");
        }
        pthread_rwlock_unlock(&rwlock);
        sleep(1);
    }
} 
/* 写操作,其它线程都不允许读或写操作 */ 
void *fun4(void *arg)
{    
    while(1)
    {  
        pthread_rwlock_wrlock(&rwlock);  
        {
            g_nNum++;  
            printf("write thread 2\n");  
        }
        pthread_rwlock_unlock(&rwlock); 

        sleep(1);  
    }  
}  
  
int main(int arc, char *argv[])  
{  
    pthread_t ThrId1, ThrId2, ThrId3, ThrId4;  
      
    pthread_rwlock_init(&rwlock, NULL);  // 初始化一个读写锁  
      
    /* 创建测试线程 */
    pthread_create(&ThrId1, NULL, fun1, NULL);  
    pthread_create(&ThrId2, NULL, fun2, NULL);  
    pthread_create(&ThrId3, NULL, fun3, NULL);  
    pthread_create(&ThrId4, NULL, fun4, NULL);  
      
    /* 等待线程结束,回收其资源 */
    pthread_join(ThrId1, NULL);  
    pthread_join(ThrId2, NULL);  
    pthread_join(ThrId3, NULL);  
    pthread_join(ThrId4, NULL);  
      
    pthread_rwlock_destroy(&rwlock);      // 销毁读写锁  
      
    return 0;  
}
  • 结果
    在这里插入图片描述

自旋锁

  • 自旋锁与互斥锁功能相同,唯一不同的就是互斥锁阻塞后休眠不占用CPU,而自旋锁阻塞后不会让出CPU,会一直忙等待,直到得到锁
  • 自旋锁在用户态较少用,而在内核态使用的比较多
  • 自旋锁的使用场景:锁的持有时间比较短,或者说小于2次上下文切换的时间
  • 自旋锁在用户态的函数接口和互斥量一样,把pthread_mutex_lock()/pthread_mutex_unlock()中mutex换成spin,如:pthread_spin_init()

自旋锁函数

  • linux中的自旋锁用结构体spinlock_t 表示,定义在include/linux/spinlock_type.h。自旋锁的接口函数全部定义在include/linux/spinlock.h头文件中,实际使用时只需include<linux/spinlock.h>即可

示例

    include<linux/spinlock.h>
    spinlock_t lock;      //定义自旋锁
    spin_lock_init(&lock);   //初始化自旋锁
    spin_lock(&lock);      //获得锁,如果没获得成功则一直等待
    {
        .......         //处理临界资源
    }
    spin_unlock(&lock);     //释放自旋锁

条件变量

  • 条件变量用来阻塞一个线程,直到条件发生。通常条件变量和互斥锁同时使用。条件变量使线程可以睡眠等待某种条件满足。条件变量是利用线程间共享的全局变量进行同步的一种机制。

  • 条件变量的逻辑:
    一个线程挂起去等待条件变量的条件成立,而另一个线程使条件成立。

基本原理

线程在改变条件状态之前先锁住互斥量。如果条件为假,线程自动阻塞,并释放等待状态改变的互斥锁。如果另一个线程改变了条件,它发信号给关联的条件变量,唤醒一个或多个等待它的线程。如果两进程共享可读写的内存,条件变量可以被用来实现这两进程间的线程同步

示例

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

pthread_cond_t taxicond = PTHREAD_COND_INITIALIZER;  
pthread_mutex_t taximutex = PTHREAD_MUTEX_INITIALIZER;  
  
void *ThrFun1(void *name)  
{  
    char *p = (char *)name;  
    
    // 加锁,把信号量加入队列,释放信号量
    pthread_mutex_lock(&taximutex); 
    {
        pthread_cond_wait(&taxicond, &taximutex);  
    } 
    pthread_mutex_unlock(&taximutex);  

    printf ("ThrFun1: %s now got a signal!\n", p);  
    pthread_exit(NULL);  
}  
  
void *ThrFun2(void *name)  
{  
    char *p = (char *)name;  
    printf ("ThrFun2: %s cond signal.\n", p);    // 发信号
    pthread_cond_signal(&taxicond);  
    pthread_exit(NULL);  
}  
  
int main (int argc, char **argv)  
{  
    pthread_t Thread1, Thread2;  
    pthread_attr_t threadattr;
    pthread_attr_init(&threadattr);  // 线程属性初始化
  
    // 创建三个线程 
    pthread_create(&Thread1, &threadattr, ThrFun1, (void *)"Thread1");  
    sleep(1);  

    pthread_create(&Thread2, &threadattr, ThrFun2, (void *)"Thread2");  
    sleep(1);   

    pthread_join(Thread1, NULL);
    pthread_join(Thread2, NULL);
  
    return 0;  
}
  • 结果
    在这里插入图片描述

虚假唤醒

  • 当线程从等待已发出信号的条件变量中醒来,却发现它等待的条件不满足时,就会发生虚假唤醒。之所以称为虚假,是因为该线程似乎无缘无故地被唤醒了。但是虚假唤醒不会无缘无故发生:它们通常是因为在发出条件变量信号和等待线程最终运行之间,另一个线程运行并更改了条件

避免虚假唤醒

  • 在wait端,我们必须把判断条件和wait()放到while循环中
    pthread_mutex_lock(&taximutex); 
    {
        while(value != wantValue)
        {
            pthread_cond_wait(&taxicond, &taximutex);  
        }
    } 
    pthread_mutex_unlock(&taximutex); 

信号量

  • 信号量用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源的访问。编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于0时,则可以访问,否则将阻塞
#include <semaphore.h>

// 初始化信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 信号量P操作(减 1)
int sem_wait(sem_t *sem);

// 以非阻塞的方式来对信号量进行减1操作
int sem_trywait(sem_t *sem);

// 信号量V操作(加 1)
int sem_post(sem_t *sem);

// 获取信号量的值
int sem_getvalue(sem_t *sem, int *sval);

// 销毁信号量
int sem_destroy(sem_t *sem);

示例

// 信号量用于同步实例
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>

sem_t sem_g,sem_p;   //定义两个信号量
char s8Test = 'a'; 

void *pthread_g(void *arg)  //此线程改变字符的值
{    
    while(1)
    {
        sem_wait(&sem_g);
        s8Test++;
        sleep(2);
        sem_post(&sem_p);
    }
} 
void *pthread_p(void *arg)  //此线程打印字符的值
{    
    while(1)
    {
        sem_wait(&sem_p);        
        printf("%c",s8Test);
        fflush(stdout);
        sem_post(&sem_g);
    }
} 
int main(int argc, char *argv[])
{    
    pthread_t tid1,tid2;
    sem_init(&sem_g, 0, 0); // 初始化信号量为0
    sem_init(&sem_p, 0, 1); // 初始化信号量为1
    
    pthread_create(&tid1, NULL, pthread_g, NULL);
    pthread_create(&tid2, NULL, pthread_p, NULL); 

    pthread_join(tid1, NULL);
    pthread_join(tid2, NULL);    
    return 0;
}
  • 结果
    在这里插入图片描述

👇👇👇👇👇 点击下方Linux兵工厂,关注去领取海量Linux学习资料和硬核文章 👇👇👇👇👇
在这里插入图片描述
-------------------------------------------------------------------------------👇👇👇👇👇----------------------------------

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

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

相关文章

SpringBoot+ActiveMQ-发布订阅模式(消费端)

ActiveMQ消息中间件的发布订阅模式 主题 topictopic生产端案例(配合topic消费端测试)&#xff1a;SpringBootActiveMQ Topic 生产端ActiveMQ版本&#xff1a;apache-activemq-5.16.5案例源码:SpringBootActiveMQ-发布订阅DemoSpringBoot集成ActiveMQ Topic消费端的pom.xml<?…

git之工作区暂存区和仓库区

工作区暂存区和仓库区 工作区 对于添加、修改、删除文件的操作&#xff0c;都发生在工作区中 暂存区 暂存区指将工作区中的操作完成小阶段的存储&#xff0c;是版本库的一部分 仓库区 仓库区表示个人开发的一个小阶段的完成 仓库区中记录的各版本是可以查看并回退的但是在暂…

uniapp小程序基于Android+vue校园考研论坛php

系统功能 本灾情救援系统主要包括两大功能模块&#xff0c;即用户功能模块和管理员功能模块。 &#xff08;1&#xff09;管理员模块&#xff1a;主要模块包括首页&#xff0c;个人中心&#xff0c;会员管理&#xff0c;话题分类管理&#xff0c;考研论坛管理&#xff0c;系统管…

python—xlwt模块详解

一、前言 xlwt模块是python中专门用于写入Excel的拓展模块&#xff0c;可以实现创建表单、写入指定单元格、指定单元格样式等人工实现的功能&#xff0c;一句话就是人使用excel实现的功能&#xff0c;这个扩展包都可以实现。 二、基础操作 1、创建workbook(创建excel) #创建…

识别项目风险的7种方法

成功的项目经理有一个共同的特点&#xff1a;能够识别和管理风险。本文主要讨论识别项目风险的七个方法。 何时识别风险 在项目开始的时候&#xff0c;风险暴露是最大的&#xff0c;不确定性很高。因为项目在开始的时候&#xff0c;信息比较少。明智的项目经理会在项目早期就…

python基础 | Numpy基础

文章目录&#x1f4da;数组操作&#x1f407;np数组的构造&#x1f955;np数组的构造&#x1f955;特殊补充&#x1f407;np数组的变形和合并&#x1f955;转置&#x1f955;合并操作&#x1f955;维度变换&#x1f407;np数组的切片和索引&#x1f955;一维数组索引与切片&…

嵌套走马灯Carousel

Carousel 的应用很广泛&#xff0c;基础用法这里不多做阐述&#xff0c;感兴趣的可以去element-gui了解Carousel 组件。 今天主要是梳理嵌套走马灯的逻辑&#xff0c;背景如下&#xff1a; 需要对项目做一个展示&#xff0c;项目可能有一个或多个&#xff0c;同时一个项目可能…

2023爱分析·云原生智能运维中台市场厂商评估报告:秒云(miaoyun.io)

目录 1. 研究范围定义 2. 云原生智能运维中台市场定义 3. 厂商评估&#xff1a;秒云&#xff08;miaoyun.io&#xff09; 4. 入选证书 1. 研究范围定义 数字化时代&#xff0c;应用成为企业开展各项业务的落脚点。随着业务的快速发展&#xff0c;应用的功能迭代变得越…

Scala集合详解(第七章:集合、数组、列表、set集合、map集合、元组、队列、并行)(尚硅谷笔记)

集合第七章:集合7.1 集合简介7.1.1 不可变集合继承图7.1.2 可变集合继承图7.2 数组7.2.1 不可变数组7.2.2 可变数组7.2.3 不可变数组与可变数组的转换7.2.4 多维数组7.3 列表 List7.3.1 不可变 List7.3.2 可变 ListBuffer7.4 Set 集合7.4.1 不可变 Set7.4.2 可变 mutable.Set7.…

蓝牙运动耳机什么牌子的好、运动蓝牙耳机排行榜推荐

近些年&#xff0c;户外运动兴起&#xff0c;运动耳机迎来爆发增长&#xff0c;拒绝运动乏味&#xff0c;追求健康运动方式&#xff0c;已经成为当下年轻人的共同诉求。跑步骑行听音乐&#xff0c;已经是运动爱好者再熟悉不过的操作&#xff0c;很多人在运动中离不开音乐的节奏…

双检测人脸防伪识别方法(活体检测+人脸识别+关键点检测+人像分割)

双检测人脸防伪识别=人脸检测+活体检测+人脸识别 1.人脸关键点+语义分割 使用mediapipe进行视频人脸关键点检测和人像分割: import time import cv2 import mediapipe as mp import numpy as npmp_drawing = mp.solutions.drawing_utils mp_drawing_styles = mp.solution…

定了:Python3.7,今年停止更新~

大家好&#xff0c;这里是程序员晚枫。 今天给大家分享一个来自Python官网的重要消息&#xff1a;Python3.7马上就要停止维护了&#xff0c;请不要使用了&#xff01; 官网链接&#xff1a;https://devguide.python.org/versions/ 停更的后果是什么&#xff1f; 周末翻阅Py…

从功能测试(点点点)到进阶自动化测试,实现薪资翻倍我只用了3个月时间

前言 从事测试工作已3年有余了&#xff0c;今天想聊一下自己刚入门时和现在的今昔对比&#xff0c;虽然现在也没什么成就&#xff0c;只能说笑谈一下自己的测试生涯&#xff0c;各位看官就当是茶余饭后的吐槽吧&#xff0c;另外也想写一写自己的职场感想&#xff0c;希望对刚开…

实例4:树莓派GPIO控制舵机转动

实例4&#xff1a;树莓派GPIO控制舵机转动 实验目的 通过背景知识学习&#xff0c;了解舵机的外观及基本运动方式。了解四足机器人mini pupper腿部单个舵机的组成结构。通过GPIO对舵机进行转动控制&#xff0c;熟悉PWM。了解mini pupper舵机组的整体调零。 实验要求 使用Py…

【vue2每日小知识】实现directive自定义指令的封装与全局注册

&#x1f973;博 主&#xff1a;初映CY的前说(前端领域) &#x1f31e;个人信条&#xff1a;想要变成得到&#xff0c;中间还有做到&#xff01; &#x1f918;本文核心&#xff1a;将我们的自定义指令directive统一管理并批量注册 目录 一、directive自定义指令介绍 二…

LeetCode——752. 打开转盘锁

一、题目 你有一个带有四个圆形拨轮的转盘锁。每个拨轮都有10个数字&#xff1a; ‘0’, ‘1’, ‘2’, ‘3’, ‘4’, ‘5’, ‘6’, ‘7’, ‘8’, ‘9’ 。每个拨轮可以自由旋转&#xff1a;例如把 ‘9’ 变为 ‘0’&#xff0c;‘0’ 变为 ‘9’ 。每次旋转都只能旋转一个…

Python每日一练(20230222)

目录 1. 柱状图中最大的矩形 2. 汇总区间 3. 寻找旋转排序数组中的最小值 1. 柱状图中最大的矩形 难度&#xff1a;困难 给定 n 个非负整数&#xff0c;用来表示柱状图中各个柱子的高度。每个柱子彼此相邻&#xff0c;且宽度为 1 。 求在该柱状图中&#xff0c;能够勾勒出…

IDEA工具系列之连接Linux

我们在开发的时候&#xff0c;用IDEA开发程序&#xff0c;用XSHELL来管理服务器&#xff0c;这两个工具切换比较麻烦。有没有用IDEA来连接Linux。当然有&#xff0c;下面有实践步骤&#xff1a; 首先&#xff1a;连接Linux 打开IDEA->Tools->Start SSH Session 其中1&…

MySQL------窗口函数

一、窗口函数定义 1、 适用场景及分类 怎么样得到各部门工资排名前N名员工列表? 查找各部门每人工资占部门总工资的百分比&#xff1f; 累计求和如何计算&#xff1f; 连续统计N天登陆的用户&#xff1f;以上类型问题均使用窗口函数可以解决&#xff0c;这类需求都有一个共同…

51单片机串口使用

文章目录前言一、串口概念二、中断中的RI、TI和SUBF1.RI2.TI3.SUBF三、串口波特率的计算四、代码的编写总结前言 今天将为大家讲解51单片机的串口原理及代码编写。 一、串口概念 51单片机串口是一种通信接口&#xff0c;它可以将51单片机与外部设备连接起来&#xff0c;实现…