云计算百科
云计算领域专业知识百科平台

【Linux】一文带你理解清楚 同步与互斥(附大量C++理解代码和理解图片,逻辑清晰-通俗易懂)

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”

绪论​: 本章是Linux线程中非常重要的概念,它不仅在Linux中非常重要同样也是在日常工作项目中常用的两种方法,通过互斥防止多个线程访问同一个资源时导致数据的问题,以及再次通过同步的关系让多个线程之间的互斥更加的有序,本章将通过知识 + 实例 的方式带你轻松认识清楚到底什么是常见的互斥和同步。 ———————— 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。

1.线程的互斥

背景

在程序中部分资源是共享的,如全局的东西所有线程都能访问得到。 对此当多个线程同时访问这种共享资源时,就可能导致数据不一致。

解决方法:

  • 任何一个时刻,只允许一个线程正在访问共享资源—临界资源(也就共享的数据)
  • 我们把访问进程中访问临界资源的代码—临界区(被保护的区域)
  • 也就有了概念:

    • 互斥:如何时刻,互斥保证有且只有一个执行流进入临界区
    • 原子性:不会被任何调度机制打断的操作,该操作只有两态,要么完成,要么未完成

    1.1 互斥的案例(了解)

    对于c/c++代码会经过编译变成汇编,而某些代码的底层可能不止一条汇编语句就肯定不是原子的(因为要同时考虑三个语句的真假)如下面的变量a++的底层:在这里插入图片描述

    因为时间片切换,可能在一个代码的底层的多个语句中的第二个语句时间片就到了,导致未执行完就切走了,走之前会保存好上下文数据。轮到执行其他线程,假设其他线程也同样访问相同的内存资源,并修改了该资源,当后面时间片到了。又轮到一开始的线程,他会继续从未完成的部分开始,而非先读取内存数据,这样就会导致前面的线程的任务白做(因为一开始的线程的数据时之前的数据,他修改后就会导致数据回到之前)! 在这里插入图片描述 多线程并发访问全部整形的汇编,若不是原子的,就会有数据不一致的并发问题。

    其中认识到CPU中执行过程:

  • 算:算术运算
  • 逻:逻辑运算(真假)
  • 中:处理内外中断
  • 控:控制单元(时钟控制)
  • 其中 if 判断语句同样不是原子的(本质也和上面的++一样需要到CPU中去处理判断,这样汇编语句就有多个)!数据在内存中,本质是被线程共享的,数据被读取到寄存器汇总,本质变成了线程的上下文,它是属于线程私有数据数据。

    1.2 互斥锁 函数(了解)

  • 定义锁:

  • 全局锁:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER 此时定义的全局锁就能直接使用,并且不用销毁。
  • 初始化锁:int pthread_mutex_init(pthread_mutex_t *restrict mutex, const pthread_mutexattr_t *restrict attr) attr是锁的属性,暂不考虑写成nullptr
  • 销毁锁:

  • int pthread_mutex_destroy(pthread_mutex_t *mutex)
  • 当在局部创建一把锁时,需要对其进行初始化操作:

  • pthread_mutex_init
  • pthread_mutex_destory,也就代表使用完后需要摧毁
  • 因为自定义的函数会使用到锁所以需要把它当参数传递进去

  • 加锁:pthread_mutex_lock(pthread_mutex_t*mutex)

  • 解锁:pthread_mutex_unlock(pthread_mutex_t*mutex)

  • 申请成功返回0,失败返回错误码

    注意事项:

  • 加锁时我们要尽可能的少给代码块加锁
  • 一般加锁是给临界区加锁
  • 申请锁本身是安全,原子的
  • 一旦有了公共资源,程序员就需要保证是加锁的!
  • 根据互斥的定义:

    任何时刻只允许一个线程申请锁成功!其他线程申请失败时,会在mutex锁上进行阻塞,本质也就等待。

    也就推出了不阻塞的锁:

  • 不阻塞的申请锁:pthread_mutex_trylock(pthread_mutex_t*mutex)
  • 线程在临界区中访问加锁的临界资源的时候是可能发生切换,但即便切换了别人也仍然不能访问。(理解成,一个房间只要有钥匙才能进入,而当我们进入后我们出去时是拿着钥匙走的,别人同样还 是进不去)

    1.3 简单互斥锁(源码)

    //main.cpp
    #include<iostream>
    #include<thread>
    #include<cstdlib>
    #include"LockGuard.hpp"
    #include"Thread.hpp"
    #include<unistd.h>
    #include<vector>
    #include<string>
    using namespace std;
    // 应用方的视角

    //为了能同时传递线程名和锁变量
    //就构建ThreadData类,让其可以直接一起传递进去
    class ThreadData
    {
    public:
    ThreadData(string& name,pthread_mutex_t* lock)
    :threadname(name),pmutex(lock)
    {}
    public:
    string threadname;
    pthread_mutex_t * pmutex;
    };

    void Print(int num)
    {
    while (num)
    {
    std::cout << "hello world: " << num << std::endl;
    sleep(1);
    }
    }

    int ticket = 10000; // 全局的共享资源

    // pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //全局锁,不用init初始化

    void GetTicket(ThreadData *td)//抢票原理:
    {
    while (true)
    {

    { //通过花括号括其来的部分为一个代码块也就是确定了临界区

    //pthread_mutex_lock(mutex);//加锁
    LockGuard lockguard(td->pmutex);
    //LockGuard类会自动的申请锁,并且释放锁(因为当他构造和析构时就会执行加锁解锁操作!)
    if (ticket > 0) // 4. 一个线程在临界区中访问临界资源的时候,可不可能发生切换?可能,完全允许!!
    {
    // 充当抢票花费的时间
    usleep(1000);
    printf("%s get a ticket: %d\\n",td->threadname.c_str(),ticket);
    ticket;
    // pthread_mutex_unlock(mutex);//解锁
    }
    else
    {
    // pthread_mutex_unlock(mutex);//解锁
    break;
    }
    }

    }
    }

    string GetThreadName()
    {
    static int number = 1;
    char name[64];
    snprintf(name, sizeof(name), "Thread-%d", number++);
    return name;
    }

    int main()
    {
    pthread_mutex_t mutex;
    pthread_mutex_init(&mutex,nullptr);

    string name1 = GetThreadName();
    ThreadData *td1 = new ThreadData(name1,&mutex);//构造类

    Thread<ThreadData*> t1(td1, GetTicket,name1);
    //把ThreadData这个类做参数传递进去,在内部就能同时拿到

    string name2 = GetThreadName();
    ThreadData *td2 = new ThreadData(name2,&mutex);
    Thread<ThreadData*> t2(td2,GetTicket,name2);

    string name3 = GetThreadName();
    ThreadData *td3 = new ThreadData(name3,&mutex);
    Thread<ThreadData*> t3(td3, GetTicket, name3);

    string name4 = GetThreadName();
    ThreadData *td4 = new ThreadData(name4,&mutex);
    Thread<ThreadData*> t4(td4, GetTicket, name4);

    t1.Start();
    t2.Start();
    t3.Start();
    t4.Start();

    t1.Join();
    t2.Join();
    t3.Join();
    t4.Join();

    pthread_mutex_destroy(&mutex);

    delete td1;
    delete td2;
    delete td3;
    delete td4;

    return 0;
    }

    //Thread.hpp:
    #pragma once
    #include<iostream>
    #include<string>
    #include<functional>
    #include<pthread.h>
    using namespace std;

    //typedef function<void()> func_t

    //注意!!
    //此处的修改,因为带有模板所以后面用该类型变成fun_t<T>
    template<class T>
    using func_t = function<void(T)>;//返回值void 参数为;

    //加上模板,这个类型是给传进来的参数数据data的!
    template<class T>
    class Thread
    {
    public:
    Thread(T data, func_t<T> func, const string& name)
    :_tid(0),_name(name),_isrunning(false),_func(func),_data(data)
    {}

    //因为在类内的函数默认是有this指针的这样就会导致pthread_create的threadrotine的类型不匹配而导致的无法传参
    //所以解决方法就是改成静态函数,但此时又不能使用成员变量了,所哟把参数args改成this传递进来!
    static void *ThreadRotine(void* args)
    {
    Thread *ts = static_cast<Thread*>(args);

    ts->_func(ts->_data);
    return nullptr;
    }

    bool Start()
    {
    int n = pthread_create(&_tid,nullptr,ThreadRotine,this);
    if(n == 0)
    {
    _isrunning = true;
    return true;
    }
    else return false;
    }

    string Threadname()
    {
    return _name;
    }

    bool Join()
    {
    if(!_isrunning) return true;
    int n = pthread_join(_tid,nullptr);
    if(n==0)
    {
    _isrunning = false;
    return true;
    }
    return false;

    }

    bool Isrunning()
    {
    return _isrunning;
    }

    ~Thread()
    {}

    private:
    string _name;
    func_t<T> _func;

    pthread_t _tid;//创建自动形成
    bool _isrunning;
    T _data;
    //pid_t tid;
    };

    1.3.附加:封装锁

    • 把锁封装成一个类实现,当构造时加锁,析构时释放。
    • 这样把该类写在一个代码块中当成局部变量,这样就能让其在代码块中进行加锁,出代码块析构解锁

    LockGuard.hpp:

    #pragma once

    #include <pthread.h>

    class Mutex
    {
    public:
    Mutex(pthread_mutex_t *lock) : _lock(lock)
    {}

    void Lock()

    {
    pthread_mutex_lock(_lock);
    }
    void Unlock()

    {
    pthread_mutex_unlock(_lock);
    }

    ~Mutex()
    {}

    private:
    pthread_mutex_t *_lock; // 不定义锁,默认外面会创建传进来
    };

    class LockGuard
    {
    public:
    LockGuard(pthread_mutex_t *lock) : _mutex(lock)

    {
    _mutex.Lock();

    }
    ~LockGuard()
    {
    _mutex.Unlock();

    }

    private:
    Mutex _mutex;
    };

    上述代码就能实现一个基本的抢票机制,通过互斥锁就不会导致数据错乱

    ps:远程拷贝:scp 用户名@IP地址:路径(后输入密码即可)

    但有可能一个票被一个人全部都抢了,对其他线程就形成了饥饿问题,他的原理是一个线程在申请完锁,解锁后又立马申请锁资源。


    要结局饥饿问题,互斥是无法解决的,也就是同步解决:

    • 当一个线程申请完锁后不能再立马申请锁资(也就是有一定的顺序性),请看目录中的同步

    1.4 互斥锁的本质

    大多数体系结构都提供了swap/exchang指令,该指令的作用是把寄存器和内存单元的数据相交换

    底层汇编原理:

    exchang eax mem_addr(也就是把寄存器eax的内容和内存数据mem_addr交换的汇编语句),因为只有一条主要语句所以交换的过程是原子的,其他语句执行时即使被切换,也会把自身数据(%al寄存器的数据)带走,也就不会影响其他线程。

    互斥锁的实现原理: 在这里插入图片描述 解释:

    每个线程要执行加锁时首先都会先move 0到%al寄存器中,然后在和mutex的数据进行交换,然后判断寄存器%al中的值>0则表示加锁成功,反之则加锁失败在加锁处挂起等待! 加锁失败的情况,因为有人已经在使用该锁,所以会把内存中的mutex改为了0(他也会执行第一个move操作),当他没有归还锁资源时,mutex中的值为0,当别人一交换则%al寄存器中就会变成0就会挂起等待了。(这样也就实现了互斥锁,加锁的区域只能由一个线程使用!)

    所以加锁的本质就是:

    • 寄存器和内存数据的交换(因为只有一条语句所以交换的过程是原子的,xchgb %al mutex)

    • 解锁的原理就是基于加锁的,把内存数据mutex改回1,并唤醒等待挂起的线程即可。

    • 加锁原则:谁加锁,谁解锁


    1.5 认识:可重入 与 线程安全

    线程安全:

  • 多个线程并发同一段代码时,不会出现不同的结果。
  • 若出现问题(如:崩溃,数据异常),则是线程不安全的。
  • 其中线程安全和可重入本质是一样的,只不过可重入是函数的概念,线程安全则是线程的概念
  • 线程不安全的情况:

  • 不保护共享变量的函数(全局变量)
  • 函数状态随着被调用,状态发生变化的函数(静态变量在函数中,每当被调用就会发生改变)
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数
  • 线程安全的情况:

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的(可以理解成这个变量即使线程修改了,当线程结束后该值又会变回来相当于没变)
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性
  • 常见不可重入的情况:

  • 调用了malloc/free函数,因为malloc函数是用全局链表来管理堆的
  • 调用了标准I/O库函数,标准I/O库的很多实现都以不可重入的方式使用全局数据结构
  • 可重入函数体内使用了静态的数据结构
  • 常见可重入的情况

  • 不使用全局变量或静态变量
  • 不使用用malloc或者new开辟出的空间
  • 不调用不可重入函数
  • 不返回静态或全局数据,所有数据都有函数的调用者提供
  • 使用本地数据,或者通过制作全局数据的本地拷贝来保护全局数据
  • 可重入与线程安全联系

  • 可重入函数一定是线程安全的。
  • 函数是不可重入的,那就不能由多个线程使用,有可能引发线程安全问题
  • 如果一个函数中有全局变量,那么这个函数既不是线程安全也不是可重入的。
  • 可重入与线程安全区别

  • 可重入函数是线程安全函数的一种
  • 线程安全不一定是可重入的,而可重入函数则一定是线程安全的。
  • 如果将对临界资源的访问加上锁,则这个函数是线程安全的,但如果这个重入函数若锁还未释放则会产生死锁,因此是不可重入的。
  • 1.6 死锁问题

    原理: 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。 可以理解成,现在用了两把锁A/B各用了一把锁,但两个线程必须同时持有两把锁才能继续往后运行(如下图) 在这里插入图片描述 一个线程也可能死锁:在程序内部重复申请已经申请过的锁(此时申请不到就会被永久的挂起了)

    1.6.1死锁产生的必要条件:

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,对已获得的资源保持不放(相当于上面两个线程的情况)
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺(没有因为一个线程优先级高而强行使用所要的锁)
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系(可见上图)
  • 1.6.2避免死锁:

  • 不加锁(破坏互斥条件,但有些时候难以实现)
  • 若申请某个锁不成功后,释放自身的锁已有的锁(那么就破坏了请求与保持条件)
  • 当申请某个锁时发现他是被占用的时,直接把他解锁再加锁到当前自己线程(破坏不剥夺条件)
  • 尽量的把锁资源按顺序申请给线程 !(破坏循环等待条件)
  • 避免锁未释放的场景以及资源一次分配(一个资源配一把锁)
  • 避免死锁的算法:死锁检测算法,银行家算法。

    2.线程的同步

    背景:

    当一个线程短时间的不断的申请锁,释放锁,导致其他人长时间得不到资源,也就对其他线程产生饥饿问题,为了解决饥饿问题,归还资源后线程不能立即再次申请,再通过类似队列的结构(先进先出,也就是有顺序性)管理要申请锁资源的线程。

    对此解决这个问题的方法就是同步:

    同步:在临界资源使用安全的前提下,让多线程执行具有一定的顺序性。互斥能保证资源的安全,同步能够较为充分高效的使用资源。

    2.1 同步 + 互斥 -> CP生产者消费者模型

    (计算机领域非常重要的模型) 在这里插入图片描述

    日常生活中超市就是典型的生产者消费者模型就有生产者、消费者(如下图): 在这里插入图片描述 生产者、消费者的三大关系:

  • 生产者与生产者的关系:互斥(供应商相互竞争)
  • 消费者与消费者的关系:互斥(假如只有一份商品了而都想要这个商品那么就是竞争关系)
  • 生产者和消费者的关系:互斥(生产者放完了商品消费者才能拿商品)、同步(不能让一方持续的处理商品(不断地买/卖)都是有问题的)
  • 生产者消费者模型本质就是处理好上方的三个关系。 在这里插入图片描述 321原则:

    3.三种关系 2.两种角色(生产线程/消费进程) 1.一个交易场所(内存空间)

    2.2.1 生产者消费者模型的优势:

  • 让多执行流之间的一个执行解耦(把生产者和消费者两个线程,双方在内存空间内写或者拿数据,相当于把内存空间当成一个缓冲区可以存一部分数据,所以生产消费不需要相互等待)
  • 提高处理数据的效率(通过代码解释)
  • 2.2 条件变量

    同步的实现就是通过:条件变量(互斥中有锁一样) 在这里插入图片描述

    条件变量是类似于铃铛提醒人的工具,这个条件变量是为了避免消费者(线程)在资源还没准备好就不断的去内存申请但也拿不到资源的情况(本质就是同步的工具),因为共享空间是互斥的,这样就会导致生产者也无法放数据(饥饿)。

    条件变量:当生产者将资源准备好去提醒消费者

    • 其中消费者并不是在锁上等待资源,而是在条件变量处的一个队列(一个阻塞队列)
    • 先在条件变量处的队列中排队,当资源就绪条件变量就会唤醒消费线程
    • 当拿取后若想再拿就必须从后开始排(队列)。

    也就相当于条件变量的结构为:

    struct cond
    {
    //条件是否就绪
    int flag;
    //维护一个线程队列
    tcb_queue;
    }

    当flag表示就绪就从线程队列中唤醒一个线程。

    2.3 条件变量的函数(了解)

  • 条件变量的定义:
  • 头文件:#include <pthread.h>
  • 成功返回0,错误返回错误码
  • 条件变量的使用必须要声明和摧毁
  • 对局部条件变量摧毁:
    int pthread_cond_destroy(pthread_cond_t *cond);

    对局部条件变量进行初始化:
    int pthread_cond_init(pthread_cond_t *restrict cond,
    const pthread_condattr_t *restrict attr);

    全局的条件变量,和锁一样可以直接使用
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;

  • 线程等待(等待响铃,或者本质就是申请资源)
  • 头文件:#include <pthread.h>
  • 成功返回0,错误返回错误码
  • 在指定的条件变量cond处等待,并且还要传递一把锁mutex
    int pthread_cond_wait(pthread_cond_t *restrict cond,
    pthread_mutex_t *restrict mutex);

  • 唤醒线程(条件变量成立并):
  • 头文件:#include <pthread.h>
  • 成功返回0,错误返回错误码
  • 唤醒所有线程:
    int pthread_cond_broadcast(pthread_cond_t *cond);
    唤醒一个线程:
    int pthread_cond_signal(pthread_cond_t *cond);

    2.3.1 条件变量的基本使用:

    #include<iostream>
    #include<pthread.h>
    #include<unistd.h>
    using namespace std;

    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

    void* threadRontine(void* args)
    {
    string name = static_cast<const char*>(args);

    while(true)
    {
    // sleep(1);
    pthread_mutex_lock(&mutex);

    pthread_cond_wait(&cond,&mutex);//等待
    cout << "I am a new thread:" << name <<endl;

    pthread_mutex_unlock(&mutex);
    }
    }

    //主线程
    int main()
    {
    pthread_t t1,t2,t3;

    pthread_create(&t1,nullptr,threadRontine,(void*)"thread-1");
    pthread_create(&t2,nullptr,threadRontine,(void*)"thread-2");
    pthread_create(&t3,nullptr,threadRontine,(void*)"thread-3");

    sleep(5);//5s后让条件变量唤醒线程
    while(true)
    {
    pthread_cond_signal(&cond);//唤醒一个线程
    // pthread_cond_broadcast(&cond);//唤醒全部线程
    sleep(1);
    }

    pthread_join(t1,nullptr);
    pthread_join(t2,nullptr);
    pthread_join(t3,nullptr);

    }

    当只唤醒一个线程(pthread_cond_signal)时他是逐个的: 当唤醒全部线程(pthread_cond_broadcast)时他是所有线程一起的: 在这里插入图片描述

    从上面的饥饿问题不能发现:单纯的互斥,能保证数据的安全,但不一定合理或高效

    pthread_cond_wait函数的细节:

  • 当让线程在进行等待的时候,要自动释放申请的锁。
  • 线程被在临界区内唤醒的时候,要重新申请并持有锁。
  • 当多个线程被唤醒的时候,它们都要重新申请并持有锁,所以是要竞争锁的。
  • 调用该函数可能失败,这样就会让线程在不满足使用资源条件的前提下(队列中的资源不够多个线程分配)唤醒生产/消费线程,也称为:伪唤醒,也就是可能同时唤醒多个,但只有一个线程能拿到锁资源,其他没拿到锁资源的线程就是伪唤醒状态。(对此解决方法是将if语句换成while语句这样就能)
  • 2.4 实现:阻塞队列(及条件变量的应用)

    下面将使用到:

  • 互斥锁
  • 条件变量
  • 锁的封装
  • 将任务对象化
  • 阻塞队列(让线程实现顺序性)
  • 模拟生产者消费者模型的运行!
  • 原理:

    生产者将一个任务push到队列中,而消费者再去通过pop得到数据并处理。

    阻塞队列:

    //BlockQueue.hpp
    #pragma once
    #include<iostream>
    #include<queue>
    #include<ctime>
    #include<unistd.h>
    #include<pthread.h>
    #include"LockGuard.hpp"

    using namespace std;

    const int defaultcap = 5;//

    template<class T>
    class Blockqueue
    {
    public:
    Blockqueue(int cap = defaultcap):_capacity(cap)
    {
    pthread_mutex_init(&_mutex,nullptr);
    pthread_cond_init(&_c_cond,nullptr);
    pthread_cond_init(&_p_cond,nullptr);
    }

    bool IsFull()
    {
    return _q.size() == _capacity;
    }

    bool IsEmpty()
    {
    return _q.size() == 0;
    }
    //生产者
    void Push(const T &in)
    {
    LockGuard lockgaurd(&_mutex);
    // pthread_mutex_lock(&_mutex);
    if(IsFull())
    {
    // 生产线程,阻塞等待
    pthread_cond_wait(&_p_cond,&_mutex);
    }
    _q.push(in);

    pthread_cond_signal(&_c_cond); //放到里面被唤醒了会在锁处等待了,而非cond处,只要释放锁后就能立刻拿到锁
    // if(_q.size() > _productor_water_line) pthread_cond_signal(&_c_cond);
    // pthread_mutex_unlock(&_mutex);
    }

    void Pop(T *out)
    {
    LockGuard lockgaurd(&_mutex);
    // pthread_mutex_lock(&_mutex);
    if(IsEmpty())
    {
    //阻塞等待
    pthread_cond_wait(&_c_cond,&_mutex);
    }
    *out = _q.front();

    pthread_cond_signal(&_p_cond);
    //(_q.size() > _consumer_water_line) pthread_cond_signal(&_p_cond);
    _q.pop();

    // pthread_mutex_unlock(&_mutex);
    }
    ~Blockqueue()
    {
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_c_cond);
    pthread_cond_destroy(&_p_cond);
    }

    private:
    queue<T> _q;
    size_t _capacity;//q.size == capacity满

    pthread_mutex_t _mutex;
    pthread_cond_t _p_cond;//给生产者的
    pthread_cond_t _c_cond;//消费者

    // int _consumer_water_line;// capacity / 3 * 2
    // int _productor_water_line;//capacity / 3
    };

    封装锁:

    //LockGuard.hpp
    #pragma once
    #include <pthread.h>

    class Mutex
    {
    public:
    Mutex(pthread_mutex_t *lock) : _lock(lock)
    {}

    void Lock()

    {
    pthread_mutex_lock(_lock);
    }
    void Unlock()

    {
    pthread_mutex_unlock(_lock);
    }

    ~Mutex()
    {}

    private:
    pthread_mutex_t *_lock; // 不定义锁,默认外面会创建传进来
    };

    class LockGuard
    {
    public:
    LockGuard(pthread_mutex_t *lock) : _mutex(lock)

    {
    _mutex.Lock();

    }
    ~LockGuard()
    {
    _mutex.Unlock();

    }

    private:
    Mutex _mutex;
    };

    任务对象:

    //Task.hpp
    #pragma once
    #include<iostream>
    enum
    {
    ok = 0,
    div_zero,
    mod_zero,
    unknow
    };

    class Task
    {
    public:
    Task()
    {}

    Task(int x, int y ,char op)
    :_x(x),_y(y),_oper(op)
    {}

    void Run()
    {
    switch (_oper)
    {
    case '+':
    result = _x + _y;
    break;
    case '-':
    result = _x _y;
    break;
    case '*':
    result = _x * _y;

    break;
    case '/':
    {
    if(_y == 0) {
    code = div_zero;
    break;
    }
    result = _x / _y;
    }
    break;
    case '%':
    {
    if(_y == 0) {
    code = mod_zero;
    break;
    }
    result = _x % _y;
    }
    break;
    default:
    code = unknow;
    break;
    }
    }

    string PrintTask()
    {
    string s;
    s += to_string(_x);
    s += _oper;
    s += to_string(_y);
    s += "=?";

    return s;
    }
    void operator()()
    {
    Run();
    }
    string PrintResult()
    {
    string s;

    s += to_string(_x);
    s += _oper;
    s += to_string(_y);
    s += " =";
    s += to_string(result);
    s+= " [";
    s+= to_string(code);
    s+= "]";

    return s;
    }

    ~Task()
    {}

    private:
    int _x;
    int _y;
    char _oper;

    int result;
    int code;//任务退出码,0结果可信,!0结果不可信
    };

    主程序:

    //main.cc
    #pragma once
    #include<iostream>
    #include<queue>
    #include<ctime>
    #include<unistd.h>
    #include<pthread.h>
    #include"LockGuard.hpp"

    using namespace std;

    const int defaultcap = 5;//

    template<class T>
    class Blockqueue
    {
    public:
    Blockqueue(int cap = defaultcap):_capacity(cap)
    {
    pthread_mutex_init(&_mutex,nullptr);
    pthread_cond_init(&_c_cond,nullptr);
    pthread_cond_init(&_p_cond,nullptr);
    }

    bool IsFull()
    {
    return _q.size() == _capacity;
    }

    bool IsEmpty()
    {
    return _q.size() == 0;
    }
    //生产者
    void Push(const T &in)
    {
    LockGuard lockgaurd(&_mutex);
    // pthread_mutex_lock(&_mutex);
    //if(IsFull())
    while(IsFull())//把if改成while这样即使返回来了,也要判断数据是否能再放数据
    {
    // 生产线程,阻塞等待
    pthread_cond_wait(&_p_cond,&_mutex);
    }
    _q.push(in);

    pthread_cond_signal(&_c_cond); //放到里面被唤醒了会在锁处等待了,而非cond处,只要释放锁后就能立刻拿到锁
    // if(_q.size() > _productor_water_line) pthread_cond_signal(&_c_cond);
    // pthread_mutex_unlock(&_mutex);
    }

    void Pop(T *out)
    {
    LockGuard lockgaurd(&_mutex);
    // pthread_mutex_lock(&_mutex);
    //if(IsEmpty())
    while(IsEmpty())//把if改成while这样即使返回来了,也要判断是否有数据能使用
    {
    //阻塞等待
    pthread_cond_wait(&_c_cond,&_mutex);
    }
    *out = _q.front();

    pthread_cond_signal(&_p_cond);
    //(_q.size() > _consumer_water_line) pthread_cond_signal(&_p_cond);
    _q.pop();

    // pthread_mutex_unlock(&_mutex);
    }
    ~Blockqueue()
    {
    pthread_mutex_destroy(&_mutex);
    pthread_cond_destroy(&_c_cond);
    pthread_cond_destroy(&_p_cond);
    }

    private:
    queue<T> _q;
    size_t _capacity;//q.size == capacity满

    pthread_mutex_t _mutex;
    pthread_cond_t _p_cond;//给生产者的
    pthread_cond_t _c_cond;//消费者

    // int _consumer_water_line;// capacity / 3 * 2
    // int _productor_water_line;//capacity / 3
    };

    对此为什么生产者消费者模型能提高数据处理的效率?

    在这里插入图片描述 因为对于生产者消费者模型来说,我们不能只看内部他的生产和消费过程,这里是互斥的并没有效率的提升,但是从整体来看生产者线程获取数据的过程和消费者线程处理数据的过程也是需要时间的,他们在处理这些时间时,其他线程就能同步的去执行生产/消费过程,从而实现每个线程都能高效的执行其作用。

    2.4 信号量

    该方法同样也是实现同步的工具(只不过常用条件变量,所以这里就简略了)

    在之前文章中已经写过信号量的基本概念有:

  • 信号量的本质是一把计数器
  • 申请信号的本质就是预定资源
  • PV操作是原子的! 在这里插入图片描述
  • 把公共资源不当做整体,多线程不访问临界资源的同一个区域。 对此信号量为了防止分成的n份公共资源,分给了n+k个线程, 信号量的作用就是:确定线程能否访问资源 线程信号量申请成功后,当线程需要使用时就不需要再判断资源是否就绪,直接就能使用了(申请信号量时已经判断了)

    2.4.1信号量的基本函数

  • 信号量初始化与销毁
  • 头文件:#include <semaphore.h>
  • 初始化信号量
    int sem_init(sem_t *sem, int pshared, unsigned int value);

  • sem:返回定义的信号量(输出型)
  • pshared:在线程间共享(设置为0),还是进程间
  • value:信号量初始的值
  • 销毁信号量
    int sem_destroy(sem_t *sem);

    信号量的PV操作

  • 申请信号量,P操作–:
  • int sem_wait(sem_t *sem);

    申请成功继续,失败则阻塞等待

  • 释放信号量,V操作++:
  • int sem_post(sem_t *sem);

    上述函数的返回值都是:成功为0,失败为非0错误码


    本章完。预知后事如何,暂听下回分解。

    如果有任何问题欢迎讨论哈!

    如果觉得这篇文章对你有所帮助的话点点赞吧!

    持续更新大量C++细致内容,早关注不迷路。

    赞(0)
    未经允许不得转载:网硕互联帮助中心 » 【Linux】一文带你理解清楚 同步与互斥(附大量C++理解代码和理解图片,逻辑清晰-通俗易懂)
    分享到: 更多 (0)

    评论 抢沙发

    评论前必须登录!