用多少眼泪才能让你相信 发表于 2025-3-28 07:38:58

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

https://img-blog.csdnimg.cn/c3f1a9caf4e9460e897a95245d989d86.png
逐日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry”
    绪论​:
本章是Linux线程中非常紧张的概念,它不仅在Linux中非常紧张同样也是在日常工作项目中常用的两种方法,通过互斥防止多个线程访问同一个资源时导致数据的问题,以及再次通过同步的关系让多个线程之间的互斥更加的有序,本章将通过知识 + 实例 的方式带你轻松熟悉清楚到底什么是常见的互斥和同步。
————————
早关注不迷路,话不多说安全带系好,发车啦(发起电脑观看)。
1.线程的互斥

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

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


[*]互斥:怎样时刻,互斥保证有且只有一个执行流进入临界区
[*]原子性:不会被任何调理机制打断的操作,该操作只有两态,要么完成,要么未完成
1.1 互斥的案例(了解)

对于c/c++代码会经过编译变成汇编,而某些代码的底层可能不止一条汇编语句就肯定不是原子的(因为要同时考虑三个语句的真假)如下面的变量a++的底层:https://i-blog.csdnimg.cn/blog_migrate/8cd690796731c344514c6630f693eb4f.png
   因为时间片切换,可能在一个代码的底层的多个语句中的第二个语句时间片就到了,导致未执行完就切走了,走之前会生存好上下文数据。轮到执行其他线程,假设其他线程也同样访问相同的内存资源,并修改了该资源,当后面时间片到了。又轮到一开始的线程,他会继承从未完成的部分开始,而非先读取内存数据,这样就会导致前面的线程的任务白做(因为一开始的线程的数据时之前的数据,他修改后就会导致数据回到之前)!
https://i-blog.csdnimg.cn/blog_migrate/9bbb9a603e33d9732e889e171b0baa10.png
多线程并发访问全部整形的汇编,若不是原子的,就会有数据不一致的并发问题。
其中熟悉到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;
    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寄存器的数据)带走,也就不会影响其他线程。
互斥锁的实现原理:
https://i-blog.csdnimg.cn/blog_migrate/121c15bad21cd04ae8a3263c5ab55b28.png
表明:
   每个线程要执行加锁时起首都会先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各用了一把锁,但两个线程必须同时持有两把锁才能继承往后运行(如下图)
https://i-blog.csdnimg.cn/blog_migrate/45d6bdc6bb46fdf9e6369f8f7f809a8c.png
一个线程也可能死锁:在程序内部重复申请已经申请过的锁(此时申请不到就会被永世的挂起了)
1.6.1死锁产生的必要条件:


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


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

背景:
   当一个线程短时间的不断的申请锁,释放锁,导致其他人长时间得不到资源,也就对其他线程产生饥饿问题,为了解决饥饿问题,归还资源后线程不能立刻再次申请,再通过雷同队列的布局(先进先出,也就是有次序性)管理要申请锁资源的线程。
对此解决这个问题的方法就是同步:
同步:在临界资源利用安全的条件下,让多线程执行具有一定的次序性。互斥能保证资源的安全,同步能够较为充实高效的利用资源。
2.1 同步 + 互斥 -> CP生产者消费者模型

(计算机范畴非常紧张的模型)
https://i-blog.csdnimg.cn/blog_migrate/4fd411605709ca5fd0eaccf000b26979.png
日常生活中超市就是典范的生产者消费者模型就有生产者、消费者(如下图):
https://i-blog.csdnimg.cn/blog_migrate/419ee296d3148368bec77a0f5a80172a.png
生产者、消费者的三大关系:

[*]生产者与生产者的关系:互斥(供应商相互竞争)
[*]消费者与消费者的关系:互斥(如果只有一份商品了而都想要这个商品那么就是竞争关系)
[*]生产者和消费者的关系:互斥(生产者放完了商品消费者才能拿商品)、同步(不能让一方一连的处理商品(不断地买/卖)都是有问题的)
生产者消费者模型本质就是处理好上方的三个关系。
https://i-blog.csdnimg.cn/blog_migrate/c2508ba82b71f9db399bfe6c660475be.png
321原则:
   3.三种关系
2.两种脚色(生产线程/消费进程)
1.一个生意业务场合(内存空间)
2.2.1 生产者消费者模型的上风:


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

同步的实现就是通过:条件变量(互斥中有锁一样)
https://i-blog.csdnimg.cn/blog_migrate/a5dc6403e088453a3e9fb0f40e7a2066.png
   条件变量是雷同于铃铛提示人的工具,这个条件变量是为了制止消费者(线程)在资源还没准备好就不断的去内存申请但也拿不到资源的情况(本质就是同步的工具),因为共享空间是互斥的,这样就会导致生产者也无法放数据(饥饿)。
条件变量:当生产者将资源准备好去提示消费者


[*]其中消费者并不是在锁上等待资源,而是在条件变量处的一个队列(一个阻塞队列)
[*]先在条件变量处的队列中列队,当资源就绪条件变量就会叫醒消费线程
[*]当拿取后若想再拿就必须从后开始排(队列)。
也就相当于条件变量的布局为:
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)时他是逐个的:https://i-blog.csdnimg.cn/blog_migrate/4b5c9e2b92cdfc4ecde5a7c185a67451.png
当叫醒全部线程(pthread_cond_broadcast)时他是全部线程一起的:
https://i-blog.csdnimg.cn/blog_migrate/0ad15c5162daecc9d5040f35693def95.png
   从上面的饥饿问题不能发现:单纯的互斥,能保证数据的安全,但不一定合理或高效
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
};
对此为什么生产者消费者模型能提高数据处理的效率?
   https://i-blog.csdnimg.cn/blog_migrate/e93026e36f458d9bbab8e1a84ed17956.png
因为对于生产者消费者模型来说,我们不能只看内部他的生产和消费过程,这里是互斥的并没有效率的提升,但是从团体来看生产者线程获取数据的过程和消费者线程处理数据的过程也是必要时间的,他们在处理这些时间时,其他线程就能同步的去执行生产/消费过程,从而实现每个线程都能高效的执行其作用。
2.4 信号量

该方法同样也是实现同步的工具(只不外常用条件变量,以是这里就大略了)
在之前文章中已经写过信号量的基本概念有:

[*]信号量的本质是一把计数器
[*]申请信号的本质就是预定资源
[*]PV操作是原子的!
https://i-blog.csdnimg.cn/blog_migrate/ce396ab88d0d61f528a5df9a23cd8d51.png
把公共资源不当做团体,多线程不访问临界资源的同一个地区。
对此信号量为了防止分成的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++细致内容,早关注不迷路。


免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: 【Linux】爆肝2w字一文带你理解清楚 同步与互斥(附大量C++理解代码和理解图片,逻辑清晰-普通易懂)