Linux_线程的同步与互斥

news2024/11/26 1:26:21

目录

1、互斥相关概念 

2、代码体现互斥重要性

3、互斥锁 

3.1 初始化锁 

3.2 申请、释放锁 

3.3 加锁的思想

3.4 实现加锁 

3.5 锁的原子性 

4、线程安全 

4.1 可重入函数 

4.2 死锁 

5、线程同步 

5.1 条件变量初始化 

5.2 条件变量等待队列

5.3 唤醒等待队列

5.4 实现线程同步

结语 


前言:

        在Linux下,线程是一个很重要的概念,他可以提高多执行流的并发度,而同步与互斥是对线程的一种约束行为,比如当多个线程都访问同一个资源时,若不对该资源加以保护则会导致意料之外的错误。具体的保护措施是让线程访问共享资源时具有互斥性,即当一个线程访问时别的线程无法访问,通常用互斥锁来实现。而同步是为了让多个线程具有一定的顺序来访问共享内存,保障每个线程访问资源的机会是一样的。

1、互斥相关概念 

        线程之所以需要互斥,是因为多线程在访问共享资源时,可能该资源只允许被修改一次,但是其他线程在修改的时候“刹不住车”,导致该资源被修改多次,原因就是多个线程同时访问了该资源,如下图所示:

        在概念层面上,通常把共享资源叫做临界资源。在代码层面,把访问共享资源的代码叫做临界区


        当线程有了互斥约束后,就不会出现上述a=0时继续访问a的情况,如下图:

2、代码体现互斥重要性

        在实际生活中,某些有限的物品是不能出现负数的情况的,比如抢票,票为0时是不能继续抢票的,但是当实现多线程抢票时,若没有互斥的约束,则很容易发生票为0时还在抢票,模拟抢票的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票

class threadData
{
public:
    threadData(int number)
    {
        threadname = "线程-" + to_string(number);
    }

public:
    string threadname;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets); 
            tickets--;
        }
        else
            break;
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 4; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i);
        thread_datas.push_back(td);
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }

    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }
    return 0;
}

         运行结果:

        从结果可以看到,发生了负数票的情况,原因就是上面多线程代码没有任何互斥的约束。


        对上面代码进行分析找出其临界区,全局变量ticket是临界资源,因此代码中对ticket的访问就是临界区,如下图所示:

        为了解决上面的问题,只能使用互斥约束多线程,而互斥就必须用到互斥锁。 

3、互斥锁 

         实现互斥锁的步骤:

        1、创建一个锁变量。

        2、使用接口初始化该变量。

        3、在临界区处申请该锁。

        4、临界区代码执行完后释放锁。

        5、销毁锁。

        值得注意的是:只能用一把锁限制对临界区的访问,即线程要想访问临界区,则必须申请到该锁才能访问,没有申请到锁的线程就无法访问临界区。 


        申请锁的示意图如下:

3.1 初始化锁 

        初始化锁用到的接口介绍如下:

#include <pthread.h>
//销毁锁
int pthread_mutex_destroy(pthread_mutex_t *mutex);

//初始化锁,方式1
int pthread_mutex_init(pthread_mutex_t *restrict mutex,
              const pthread_mutexattr_t *restrict attr);
//restrict mutex表示要初始化的锁
//restrict attr表示初始化的属性

//初始化锁,方式2
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
//定义在全局,则mutex锁就已经被初始化了

        pthead_mutex_t是库提供的数据类型,用于定义一个锁。方式2是一个全局变量初始化锁,若用方式2初始化一个锁则无需对该锁进行destroy。注意:若用方式2进行锁的初始化则该锁必须是全局的。

3.2 申请、释放锁 

        锁的初始化工作完成后,接下来就是申请锁,申请锁的接口介绍如下:

#include <pthread.h>

int pthread_mutex_lock(pthread_mutex_t *mutex);//申请mutex锁

int pthread_mutex_trylock(pthread_mutex_t *mutex);//申请mutex锁

int pthread_mutex_unlock(pthread_mutex_t *mutex);//释放mutex锁

        pthread_mutex_lock申请不到锁会阻塞在该函数处,而pthread_mutex_trylock申请不到锁不会阻塞,会继续执行下面代码。

3.3 加锁的思想

        申请锁就是加锁,加锁的本质是用时间换来线程安全,让线程访问临界资源时串开访问,对临界区进行加锁时尽量缩小临界区的代码量,因为临界区的代码越少,执行的速度越快,则进程被cpu挂起的概念就越低,被cpu挂起的概念低了则可以减少其他线程等的时间,因为当申请到锁的线程被挂起了,那么其他的线程就算被cpu调度了也不能执行临界区的代码(因为其他线程没有持有锁),只能干等。

3.4 实现加锁 

        对上述代码实现加锁,代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票

class threadData
{
public:
    threadData(int number , pthread_mutex_t *mutex)
    {
        threadname = "线程-" + to_string(number);
        lock = mutex;
    }

public:
    string threadname;
    pthread_mutex_t *lock;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        pthread_mutex_lock(td->lock); //申请锁
        if(tickets > 0)
        {
            usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(td->lock);//释放锁
        }
        else{
            pthread_mutex_unlock(td->lock);//释放锁

            break;
        }
        //usleep(12); //先把此处的usleep屏蔽,观察抢票现象
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;//定义一个锁
    pthread_mutex_init(&lock, nullptr);//对该锁进行初始化

    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 3; i++)
    {
        pthread_t tid;
        threadData *td = new threadData(i ,&lock);
        thread_datas.push_back(td);
        //要将锁也传给线程
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }

    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

         运行结果:

        从结果看,虽然没有出现负票的情况,但是发现只有一个线程在抢票,原因很简单,肯定是只有该进程申请到锁了,其他线程没申请到,那么为什么只有该线程能申请到,而其他线程申请不到呢?是因为这个线程刚释放完锁后他就立马再进行申请锁的动作了,他之所以可以比其他线程更快申请到锁的原因是“他离锁最近”,具体示意图如下:


        所以在一个线程释放锁后,可以手动对该线程进行sleep,让其他线程有机会去申请到锁,因此把上述代码中释放锁后面的usleep放开,就可以让其他线程申请到锁了,运行结果如下:

3.5 锁的原子性 

        从上文可以得知,当多线程访问共享资源时,若没有互斥约束,则会发生错误,所以对线程进行加锁的操作,但是锁本身也是共享资源,因为多线程都能看到锁并且申请他,那么申请锁的时候不好导致同样的问题吗?

        答案是不会,多线程访问共享资源之所以会发生意料之外的错误,是因为多线程对共享资源做修改操作的时候,这些修改操作在底层被转换成汇编语句,虽然上层看到的修改操作只有一句代码,但是在底层转换成两三句汇编指令,而cpu一次只能运算一句汇编指令,这就导致同一个操作没有真正被cpu执行完就被切换走了,等到下次继续执行该操作时,从内存中读取的数据可能已经被别的线程修改了,这就导致了意料之外的错误。而申请锁的动作只有一句汇编指令,他的状态只有两种:1、要么没申请到锁,2、要么申请到锁。不存在执行一半被切走的可能,通常把这种状态叫做原子性,因此锁是具有原子性的。

4、线程安全 

        线程安全指的是在多线程的并行下,访问某些资源时,不会导致该资源的数据损坏或出现意料之外的错误,线程与线程之间不会互相干扰对方的操作,多线程能够安全的执行下去,把这叫做线程安全。

4.1 可重入函数 

        可重入函数值得是当同一个函数被多个线程调用时,调用的结果不会产生任何的问题,比如不会导致数据损坏或者资源泄漏,则该函数被称为可重入函数,否则,是不可重入函数。

4.2 死锁 

         死锁指的是当线程申请锁时造成了循环申请,也就是说线程1要申请线程2的锁,而线程2要申请线程1的锁,造成死循环称之为死锁,具体示意图如下:

        造成死锁的四个必要条件:

1、互斥条件:一把锁只能被一个线程申请。
2、请求与保持条件:多线程之间互相申请对方的锁,但是对方就是不释放该锁。
3、不剥夺条件 :不释放对方的锁,即使要申请的锁在对方手里也不主动释放。
4、循环等待条件 : 多线程循环等待彼此的资源。

        只要不满足上面4个条件是任何一个,则就造成不了死锁。 

5、线程同步 

        线程同步的目的是让每个线程申请锁的能力是有顺序性的,即每个线程都可以公平的申请到锁,通常是定义一个条件变量,然后将线程放入等待队列中(申请的前提是该线程必须持有锁),申请到锁的线程就能够进入等待队列中等待了,进入等待队列时线程会自动释放锁,目的是让下一个线程申请锁然后也入队,因此条件变量必须搭配锁才能使用。

        将线程放入等待队列的示意图如下:


        唤醒等待队列里的线程去申请锁:

        等待队列申请锁的逻辑:首先需要唤醒该等待队列,然后队列里的第一个线程可以重新去申请锁,访问临界资源结束后,释放锁的线程会回到队列的末尾,如此逻辑就能够实现线程同步了

5.1 条件变量初始化 

        条件变量的初始化逻辑和锁的初始化逻辑相似,都是有两种初始化方式,具体接口如下:

#include <pthread.h>
//销毁条件变量
int pthread_cond_destroy(pthread_cond_t *cond);

//条件变量初始化方式1
int pthread_cond_init(pthread_cond_t *restrict cond,
              const pthread_condattr_t *restrict attr);
//restrict cond表示要初始化的条件变量的地址
//attr表示条件变量初始化的属性设置

//条件变量初始化方式2
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//在全局定义完成初始化

5.2 条件变量等待队列

        在条件变量完成初始化后,需要将线程放入条件变量的等待队列中, 这个过程只需要调用函数pthread_cond_wait即可完成,但是要注意调用该函数时当前线程必须是持有锁的,所以使用条件变量必须依赖锁,pthread_cond_wait函数介绍如下:

#include <pthread.h>

int pthread_cond_wait(pthread_cond_t *restrict cond,
              pthread_mutex_t *restrict mutex);
//cond表示将该线程放入哪个条件变量的队列
//mutex表示等待队列被唤醒后可申请的锁

        当线程调用此函数时,会释放已经申请的锁然后在等待队列中排队,所以线程的执行流会阻塞在该函数处。

5.3 唤醒等待队列

        当调用,唤醒函数介绍如下:

#include <pthread.h>

int pthread_cond_broadcast(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则整个队列都被唤醒

int pthread_cond_signal(pthread_cond_t *cond);
//cond表示要唤醒的条件变量等待队列,该函数被调用一次则只唤醒队头的线程

5.4 实现线程同步

         上文中抢票代码的逻辑是线程释放锁后对该线程进行sleep,这么做让其他线程有了申请锁的机会,其实这也是同步的一种方法,只不过sleep的时间不好控制,而现在我们无需对线程进行sleep也可以实现同步,即使用条件变量进行同步,让系统去维护同步机制,可以更好的控制同步。

        实现线程同步的代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector>
#include <unistd.h>
#include <pthread.h>

using namespace std;

int tickets = 1000; // 用多线程模拟抢票
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;//全局初始化

class threadData
{
public:
    threadData(int number , pthread_mutex_t *mutex)
    {
        threadname = "线程-" + to_string(number);
        lock = mutex;
    }

public:
    string threadname;
    pthread_mutex_t *lock;
};

void *getTicket(void *args)
{
    threadData *td = static_cast<threadData *>(args);
    const char *name = td->threadname.c_str();
    while (true)
    {
        pthread_mutex_lock(td->lock); //申请锁
        pthread_cond_wait(&cond,td->lock);//将线程放入等待队列
        if(tickets > 0)
        {
            //usleep(1000);
            printf("%s, 抢到一张票: %d\n", name, tickets);
            tickets--;
            pthread_mutex_unlock(td->lock);//释放锁
        }
        else{
            pthread_mutex_unlock(td->lock);//释放锁

            break;
        }
    }
    printf("%s 退出\n", name);
    return nullptr;
}

int main()
{
    pthread_mutex_t lock;//定义一个锁
    pthread_mutex_init(&lock, nullptr);//对该锁进行初始化

    vector<pthread_t> tids;
    vector<threadData *> thread_datas;
    for (int i = 1; i <= 3; i++)//创建3个线程
    {
        pthread_t tid;
        threadData *td = new threadData(i ,&lock);
        thread_datas.push_back(td);
        //要将锁也传给线程
        pthread_create(&tid, nullptr, getTicket, thread_datas[i - 1]);
        tids.push_back(tid);
    }
    sleep(2);//目的是让线程全部都放入队列中,然后再进行唤醒
    //唤醒队列
    while(true)
    {
        pthread_cond_signal(&cond);
    }
    //等待线程
    for (auto thread : tids)
    {
        pthread_join(thread, nullptr);
    }
    //释放空间资源
    for (auto td : thread_datas)
    {
        delete td;
    }

    pthread_mutex_destroy(&lock);
    return 0;
}

        运行结果:

        从结果可以看到,没有出现负票的情况,并且所有线程都在抢票。这里注意pthread_mutex_lock和pthread_cond_wait两个函数对锁的申请和释放逻辑,调用pthread_mutex_lock时线程会申请锁,然后调用pthread_cond_wait时,线程会释放锁,并且阻塞在该函数处等待被唤醒,被唤醒后该线程又重新申请锁,申请成功后执行临界区代码。

结语 

        以上就是关于线程的同步与互斥讲解,若使用多线程进行并发式的执行程序,那么同步和互斥是必不可少的保护措施,他保障了多线程并发执行时线程的安全,防止出现意料之外的错误,因此对临界资源进行同步和互斥是多线程执行时非常重要的一步。

        最后如果本文有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!

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

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

相关文章

探索 Java 中的 DeferredResult<Object>

个人名片 &#x1f393;作者简介&#xff1a;java领域优质创作者 &#x1f310;个人主页&#xff1a;码农阿豪 &#x1f4de;工作室&#xff1a;新空间代码工作室&#xff08;提供各种软件服务&#xff09; &#x1f48c;个人邮箱&#xff1a;[2435024119qq.com] &#x1f4f1…

(POSIX) 文件读写基础

文章目录 &#x1f5c2;️前言&#x1f4c4;ref&#x1f4c4;访问标记&#x1f5c3;️文件访问标记 &#x1f5c2;️Code&#x1f4c4;demo&#x1f4c4;分点讲解&#x1f5c3;️打开/关闭&#x1f5c3;️写&#x1f5c3;️读 &#x1f5c2;️END&#x1f31f;关注我 &#x1f…

C++ 正则库与HTTP请求

正则表达式的概念和语法 用于描述和匹配字符串的工具&#xff0c;通过特定的语法规则&#xff0c;灵活的定义复杂字符串匹配条件 常用语法总结 基本字符匹配 a&#xff1a;匹配字符aabc&#xff1a;匹配字符串abc 元字符&#xff08;特殊含义的字符&#xff09; .&#xff1a;匹…

【医学影像】RK3588+FPGA:满足远程诊疗系统8K音视频编解码及高效传输需求

医学影像 提供基于Intel平台、NXP平台、Rockchip平台的核心板、Mini-ITX主板、PICO-ITX主板以及工业整机等计算机硬件。产品板载内存&#xff0c;集成超高清编码/解码视频引擎&#xff0c;具有出色的数据处理能力和图形处理能力&#xff0c;功能高集成&#xff0c;可应用于超声…

可信推荐系统论文分享-1

《Debiasing Recommendation by Learning Identifiable Latent Confounders》

openmv学习笔记(24电赛备赛笔记)

#openmv简介 openmv一种小型&#xff0c;可编程机器视觉摄像头&#xff0c;设计应用嵌入式应用和计算边缘&#xff0c;是图传模块&#xff0c;或者认为是一种&#xff0c;具有图像处理功能的单片机&#xff0c;提供多种接口&#xff08;I2C SPI UART CAN ADC DAC &#xff0…

【BUG】已解决:Uncaught SyntaxError: Unexpected token ‘<‘

已解决&#xff1a;Could not install packages due to an EnvironmentError: [Errno 13] Permission denied 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发者社区主理人 …

如何训练出模型的推理规划能力

背景 近期opanai对AGI做了等级划分&#xff1b;等级划分意味着AGI有了一个考核定义&#xff0c;有了升级打怪的评价指标。并给出了目前openai正处在第一级&#xff0c;即将达到第二级的论断。预计在一年或者一年半内实现第二级&#xff0c;可以完成基本问题解决任务的系统。 …

抖音客户端一面

C | 字节抖音客户端一面 Http握手过程 1. 客户端问候(Client Hello) 客户端向服务器发送一个“问候”消息&#xff0c;其中包含客户端支持的SSL/TLS版本、加密算法、压缩方法以及一个随机数。 version 版本号,https也有版本号哦TLS 1.0、TLS 1.1、TLS 1.2等等 random 随机数…

【Linux】进程信号 --- 信号保存

&#x1f466;个人主页&#xff1a;Weraphael ✍&#x1f3fb;作者简介&#xff1a;目前正在学习c和算法 ✈️专栏&#xff1a;Linux &#x1f40b; 希望大家多多支持&#xff0c;咱一起进步&#xff01;&#x1f601; 如果文章有啥瑕疵&#xff0c;希望大佬指点一二 如果文章对…

Linux中进程间通信--匿名管道和命名管道

本篇将会进入 Linux 进程中进程间通信&#xff0c;本篇简要的介绍了 Linux 中进程为什么需要通信&#xff0c;进程间通信的常用方式。然后详细的介绍了 Linux 进程间的管道通信方式&#xff0c;管道通信分为匿名管道和命名管道&#xff0c;本篇分别介绍了其实现的原理&#xff…

4.Java Web开发模式(javaBean+servlet+MVC)

Java Web开发模式 一、Java Web开发模式 1.javaBean简介 JavaBeans是Java中一种特殊的类&#xff0c;可以将多个对象封装到一个对象&#xff08;bean&#xff09;中。特点是可序列化&#xff0c;提供无参构造器&#xff0c;提供getter方法和setter方法访问对象的属性。名称中…

顺序 IO 和 随机IO

顺序 IO 和 随机IO 顺序IO 和 随机IO 是计算机存储系统领域中的概念&#xff0c;主要涉及数据的读取和写入方式。这些术语通常在讨论硬盘驱动器&#xff08;HDDs&#xff09;、固态驱动器&#xff08;SSD&#xff09;以及其他存储设备的性能时使用。 顺序IO&#xff08;Sequen…

TeamViewer关闭访问密码或固定一组密码不变

TeamViewer的新UI界面变化较大&#xff0c;网上的一些信息已经不再有效&#xff0c;更新后的访问密码在如下图所示&#xff1a; 演示的版本为7.21.4—— 设置每次你的设备访问的密码

Hi6274 反激式20瓦电源芯片

HI6274为高性能多模式 PWM 反激式20瓦电源芯片。HI6274较少的外围元器件、较低的系统成本可设计出高性能的"无Y"开关电源。HI6274提供了极为全面和性能优异的智能化保护功能&#xff0c;包括逐周期过流保护、过载保护、软启动、芯片过温保护、可编程输出过压保护功能…

Kettle 登录示例 POST请求

登录接口是post请求&#xff0c;组装Body为json字符串 var body "{\"username\":\""username"\",\"password\": \""password"\",\"code\":\""verification"\",\"uuid\…

【算法/训练】:前缀和差分

&#x1f680; 前言&#xff1a; 前面我们已经通过 【算法/学习】前缀和&&差分-CSDN博客 学习了前缀和&&差分的效相关知识&#xff0c;现在我们开始进行相关题目的练习吧 1. 校门外的树 思路&#xff1a;给[0, n]的数组都标记为1&#xff0c;然后输出m行范围…

初学Mybatis之配置解析

MyBatis 中文网配置教程 mybatis-config.xml 环境配置&#xff08;environments&#xff09; 尽管可以配置多个环境&#xff0c;但每个 SqlSessionFactory 实例只能选择一种环境 可以有多个 enviroment&#xff0c;但是 enviroments default&#xff08;默认&#xff09;只…

Linux:Linux发展史

大家好&#xff01;此篇文章并非技术博文&#xff0c;而是简单了解Linux的时代背景和发展史&#xff0c;只有知其所以然才能让我们更好地让走进Liunx的世界&#xff01; 一、计算机的发展历史背景 首先我们要知道&#xff0c;早期大多数科技的进步都是以国家的对抗为历史背景的…

【优秀设计案例】基于K-Means聚类算法的球员数据聚类分析设计与实现

背景及意义 随着NBA比赛的日益竞争激烈&#xff0c;球队需要更加深入地了解球员的能力和特征&#xff0c;以制定更有效的战术和球队管理策略。而NBA球员的统计数据包含了大量有价值的信息&#xff0c;通过对这些数据进行聚类分析&#xff0c;可以揭示出球员之间的相似性和差异…