首页 > 系统相关 >【Linux】多线程(互斥 && 同步)

【Linux】多线程(互斥 && 同步)

时间:2024-07-04 23:27:09浏览次数:28  
标签:加锁 void 互斥 线程 && pthread mutex 多线程 我们

我们在上一节多线程提到没有任何保护措施的抢票是会造成数据不一致的问题的。

那我们怎么办?
答案就是进行加锁。

目录

加锁:

认识锁和接口:

初始化:

在这里插入图片描述
这个就是我们互斥锁的类型。其中互斥代表任何时刻只允许一个线程进行访问,锁代表为了实现互斥提供的一种方式。

锁不是一个单纯的内置类型,而是一个在这里插入图片描述
那么就肯定要对他进行初始化。
其中我们有两种初始化方式。
对于全局的锁,我们使用宏的方式。
在这里插入图片描述
而局部的锁我们则需要进行init初始化。

全局的所使用宏初始化后就不需要进行destory,因为随着生命周期的结束,会自动被系统回收。

但是局部的锁使用结束时要使用destory进行销毁。

加锁 && 解锁:

在这里插入图片描述
对于锁我们现在要有一个理解:对于多线程时,每个线程都会竞争这把锁,但只有一人会竞争成功,失败的会被阻塞,直到锁被解锁。

我们的锁是进行保护临界区的,或者说是保护临界资源。

而我们保护临界资源,本质是对临界区代码进行保护,怎么样理解这句话呢?

我们所有的资源都是通过代码进行访问的,所以本质上就是把访问资源的代码保护起来。

在这里插入图片描述
加锁之后当然要进行解锁在这里插入图片描述
所以我们就来改进一下上一章节产出的封装线程库 + 抢票的代码。

全局的方式:

我们要先看一下错误的加锁方式:
在这里插入图片描述
现象:
在这里插入图片描述
原因:因为只有一个线程会抢到锁,而对于上图程序而言一旦加锁就势必要把全部的票数抢完才可以解锁,也就意味着别的线程都无法抢票了。

所以上图的加锁方式是错误的,失去了多线程的意义。

正确的加锁:

在这里插入图片描述
现象:
在这里插入图片描述
在这里插入图片描述

结论:

  1. 加锁的范围要小粒度,非临界区是并行执行,临界区是串行执行,当你的粒度过大,串行的就多了,效率就低下了。
  2. 任何线程,进行抢票都需要申请锁,并不能因为程序是你写的而是个别线程出现特例。
  3. 所有的线程都申请锁,前提是所有的线程都可以看到这把锁,这意味着锁是共享资源,如何保证锁的安全?锁是原子的!
  4. 原子性:要么没做,要么就是做完,没有中间状态。他的反例就是吃饭,吃饭有没吃,也有吃了,但是还有吃饭中.
  5. 线程申请锁失败了就要被阻塞
  6. 线程申请锁成功继续运行
  7. 如果线程申请锁成功了,在执行临界区代码,在执行临界区代码期间可以被切换吗?
    答案是可以的,并且其他线程依旧无法进入,因为被切换的进程带着锁走了并没有释放!

结论:对于没有锁的线程,只有申请了锁的线程释放了线程才是有意义的。

其实对于访问临界区,对于无锁线程来说也是原子的。

局部的方式:

局部锁我们就要修改一下上节课的代码:
首先创建mythread时就不能单纯的只有name,还要再多一个mutex指针,于是我们选择传一个结构体指针即可。
在这里插入图片描述
在这里插入图片描述
具体变动如下,随着传入数据的修改也要修改一下连带的部分,造成了牵一发动全身的情形。
而引入了模板就可以避免这些问题,这就是模板的好处,但是由于这里我们并没有使用模板,所以暂时只能这样

#include <iostream>
#include <string>
#include <unistd.h>
#include <cstdio>
#include <pthread.h>

class ThreadData
{
public:
    ThreadData(const std::string name, pthread_mutex_t* mutex) : _name(name), _mutex(mutex)
    {
    }

public:
    std::string _name;
    pthread_mutex_t* _mutex;
};
namespace cyc
{

    class mythread
    {
    public:
        typedef void (*func_t)(ThreadData*);
        mythread(ThreadData* td, func_t func) : _td(td), _func(func), _isRunning(false)
        {
        }
        ~mythread()
        {
        }

        void Excute()
        {
            _isRunning = true;
            _func(_td);
            _isRunning = false;
        }
        static void *routine(void *arg)
        {
            mythread *self = static_cast<mythread *>(arg);
            self->Excute();
            return nullptr;
        }

        void Start()
        {
            int n = ::pthread_create(&_tid, nullptr, routine, (void *)this);
            if (n != 0)
            {
                perror("pthread_create fail");
                exit(1);
            }
        }
        void Stop()
        {
            if (_isRunning)
            {
                pthread_cancel(_tid);
                _isRunning = false;
            }
        }
        void Join()
        {
            int n = pthread_join(_tid, nullptr);
            if (n != 0)
            {
                perror("pthread_join fail");
                exit(1);
            }
            std::cout << _td->_name << " Join sucess..." << std::endl;
            delete _td;
        }
        std::string GetStatus()
        {
            if (_isRunning)
                return "Running";
            else
                return "sleeping";
        }

    private:
        ThreadData *_td;
        pthread_t _tid;
        func_t _func;
        bool _isRunning;
    };
}

传参时new一下
在这里插入图片描述
加锁时直接找td的成员即可。
在这里插入图片描述
此外我们还有另一种加锁方式,称之为RAII风格。
在这里插入图片描述
定义一个局部变量,当此时循环开始或者结束时自动创建,而正好这个类的构造与析构包含了lock与unlock,避免了繁琐的上下锁。
在这里插入图片描述

原理角度理解:

其实我们在加锁与解锁已经说明了很大一部分原理了。
在这里插入图片描述

接下来我们要探讨一下为什么lock后,
申请到锁的线程会继续执行程序,而其他线程会阻塞住?
最主要的原因就是申请到锁的线程在lock中会返回一个值,从而继续运行,而申请失败的线程则会不返回,线程就是阻塞了。

另外,我们的lock函数与pthread_mutex_t这个类型都是Pthread库中的,这个库会维护这套东西,当申请失败就会线程状态设置为S,放入等待队列中,当申请成功的线程unlock后,阻塞在lock函数内部的线程被重新唤醒,继续申请锁,重复以上步骤。

实现角度理解:

到实现层面上我们就必须谈谈原子性。

原子性在概念上是两态的,一条汇编就是原子的,他有多种体现形式,比如我们说过的抢票代码。

这里插个嘴,比较深的了解一下硬件,对整体节奏无影响:

我们的程序经过编译之后形成汇编,就像printf->十几行汇编->二进制:此时我们的二进制由两部分构成,一部分是二进制数据(int float…),一部分是二进制指令(被CPU进行执行)。
可是CPU怎么认识二进制指令?
CPU除了有寄存器构成,还有硬件电路构成,其中我们的数据寄存器中存储着数据,当我们进行加减乘除时怎么操作?CPU具有指令集,指令集可以知道执行什么操作,但具体怎么执行要靠硬件电路。
所以CPU在设计时就存在指令集这一概念。
因为CPU最初可以认识加减乘除,所以我们最开始的程序是由二进制编写的,比如纸带…–>但是效率太低,我们就有了汇编,也就有了编译器-------->C/C++。
这些语言都在指令集的基础上才得以存在。

为了实现互斥锁操作,大多数体系结构(CPU)都提供了swap或exchange指令,该指令的作用是把寄存器和内存单元的数据相交换,由于只有一条指令,保证了原子性,即使是多处理器平台,访问内存的 总线周期也有先后,一个处理器上的交换指令执行时另一个处理器的交换指令只能等待总线周期。 现在我们把lock和unlock的伪代码看一下
在这里插入图片描述
现在我们将这个伪代码走一遍可以更好的感受锁(下图是一些辅助知识帮助理解)。

在这里插入图片描述

在这里插入图片描述
具体步骤:
在这里插入图片描述

同步:

我们可以观察到抢票的结果:其中一个线程抢票很频繁,这也是我们同步要解决的问题。

我们现在就要举例子进行一个形象的解释:

标签:加锁,void,互斥,线程,&&,pthread,mutex,多线程,我们
From: https://blog.csdn.net/2301_78636079/article/details/140086741

相关文章

  • 多线程笔记
    目录1.概念进程与线程的区别:多线程的概念 多线程优缺点​编辑创建多线程的几种方式1、继承Thread类2、实现Runnable接口3、实现Callable接口实现Runnable和Callable有什么区别:相同点:不同点:注意点:4、线程池实现多线程创建线程的方式的比较实现runnable和继承t......
  • python爬虫3-多进程多线程协程
    多进程和多线程frommultiprocessingimportProcessimportthreadingdefprocess_worker():foriinrange(200):print(f"Processworker{i}")defthread_worker():foriinrange(200):print(f"Threadworker{i}")if__......
  • “Java多线程编程:从Thread到Runnable再到Callable的深入探索“
    1什么是进程?通俗地解释为:计算机中正在执行的一个程序实例。进程它是系统分配资源的基本单位。想象一下,你的电脑就像是一个大工厂,而每一个进程就像是这个工厂里的一条生产线或者一个工作小组,它们各自独立地运行着不同的任务,但同时又受到整个工厂(即操作系统)的管理和调度。......
  • java使用Netty实现TCP收发消息的例子,多线程并且含断线自动重连
    需求:有一个TCP的服务,需要使用Netty开发一个TCP连接并收发消息的程序。要求多线程并且含断线自动重连能力。组织结构,使用JavaMaven编程方式功能还包含读取配置文件和log4j2写日志部分 完整代码:App.javapackagecom.LSpbxServer;importorg.slf4j.Logger;import......
  • 多线程编程的基本概念,C++标准库中的多线程支持(std::thread,std::async),如何处理线程同步
    多线程编程在现代计算机系统中非常重要,因为它能够使程序同时执行多个操作,提高计算效率。以下是多线程编程的基本概念及如何在C++标准库中使用std::thread和std::async进行多线程编程,同时处理线程同步和并发问题。多线程编程的基本概念线程(Thread):线程是一个轻量级的进程,是......
  • JAVA多线程快速入门
    什么是多线程概述线程线程是操作系统能够进行运算调度的最小单位它被包含在进程之中,是进程中的实际运作单位简单理解应用软件中互相独立,可以同时运行的功能进程进程是程序的基本执行实体/系统分配资源的基本单位作用充分利用cpu提......
  • Java多线程编程
    1.进程进程是指操作系统中正在运行的程序实例,它是系统资源分配的基本单位。每个进程都拥有独立的内存空间和系统资源,可以看作是程序的一次执行过程。2.线程线程是进程中的执行单元,也被称为轻量级进程(LightWeightProcess)。一个进程可以包含多个线程,这些线程共享进......
  • IO线程-同步、互斥、条件变量
    1.同步1.1概念同步(synchronization)指的是多个任务(线程)按照约定的顺序相互配合完成一件事情(异步:异步则反之,并非一定需要一件事做完再做另一件事。)1.2同步机制通过信号量实现线程间同步。信号量:通过信号量实现同步操作;由信号量来决定线程是继续运行还是阻塞等待.信......
  • Winform SynchronizationContext多线程更新画面控件
    SynchronizationContext在通讯中充当传输者的角色,实现功能就是一个线程和另外一个线程的通讯。需要注意的是,不是每个线程都附加SynchronizationContext这个对象,只有UI线程是一直拥有的。故获取SynchronizationContext也只能在UI线程上进行SynchronizationContextcontex......
  • Linux多进程和多线程(一)-进程的概念和创建
    进程进程的概念进程的特点如下进程和程序的区别LINUX进程管理getpid()getppid()进程的地址空间虚拟地址和物理地址进程状态管理进程相关命令pstoppstreekill进程的创建并发和并行fork()父子进程执行不同的任务创建多个进程进程的退出exit()和_exit()exit()函数......