多线程的理解

1 .简介:

理解线程

  • 一般教材:线程是在进程内部运行的一个执行分支,属于进程的一部分,粒度要比进程更细和轻量化
  • 我们也不用理解进程的方式理解线程

在这里插入图片描述
我们现在这样来看

  • 一个PCB就是一个需要调度的执行流
  • Linux下没有专门为线程设置TCB,而是用进程的PCB来模拟线程
  • 不用设计TCB,我们就不用维护复杂的进程和线程的关系,不用单独为线程设计任何算法,直接使用进程的一套相关方法。OS只需聚焦在进程的资源分配上

在这里插入图片描述
一般教材:线程是在进程内部运行的一个执行分支,属于进程的一部分,粒度要比进程更细和轻量化

  • 线程在进程的内部,这个内部指的式进程的地址空间内部运行
  • 执行分支:CPU调度的时候,只看PCB,每一个PCB曾经被指派过指向方法和数据,CPU可以直接的调度

进程

今天的进程 VS 之前的进程

之前的进程,内部只有一个执行流的进程

  • 今天的进程,内部可以有多个执行流

  • 进程有地址空间,有页表,而创建线程不需要有地址空间页表
    创建线程只需要创建PCB,然后把资源分配给线程即可

  • 总结:创建进程的“成本”非常搞,成本:时间+空间
    创建进程要使用的资源是非常多的
    内核视角:
    进程是承担分配系统资源的基本实体
    线程是CPU调度的基本单位,承担进程资源的一部分的基本实体
    进程划分资源给线程
    在这里插入图片描述

Linux线程与接口关系的认识

Linux PCB <= 传统意义上的进程PCB
Linux的进程是轻量级的进程
在这里插入图片描述

  • 进程,独立性,可以有部分共享资源(管道,ipc资源)
  • 线程,大部分资源是共享的,可以有部分资源是“私有的”(pbc,栈,上下文)
    在这里插入图片描述

pthread_create

在这里插入图片描述

在这里插入图片描述
发现只有一个进程,并且kill以后,两个线程都停止跑了
在这里插入图片描述
使用ps -aL,L选项是查看轻量级进程
他们的PID相同(证明是同一个进程内部),LWP不同(linht weight process)
Linux OS 调度的时候看的是PID 还是LWP?
LWP
如何理解我们之前单独一个进程的亲狂?? PID = LWP
linux没有真正意义上的线程
在这里插入图片描述

线程的优点

在这里插入图片描述

计算密集型场景
线程是不是越多越好??
不一定!
如果线程太多,会导致线程间过度调度切换(有成本的)
I/O密集型情况下
线程是不是越多越好?
也是不一定
不过,IO允许创建多一些进程(不是无脑多)
大多的情况下IO都是在等待的

多进程和多线程共同的缺点

在这里插入图片描述

线程异常

在这里插入图片描述

线程用途

在这里插入图片描述

2 .线程接口(pthread库)

线程创建

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
一次创建多个线程
在这里插入图片描述
在这里插入图片描述

线程等待

在这里插入图片描述

不要认为这边的返回值之恩那个是int,只要不是临时变量,了也可以是其他变量,对象的地址

在这里插入图片描述

在这里插入图片描述

线程终止

线程终止的方案:

  1. 函数中return
    a .main函数退出return时候代表主线程/进程退出
    b .其他线程return,只代表当前线程退出
  2. 新线程通过pthread_exit终止自己 (vs exit是终止进程,不要在其他线程中调用,如果你只要想终止一个线程的时候)
  3. 取消目标进程 pthread_cancel

一般推荐前面两种

4. 在这里插入图片描述

监控脚本:
while :; do ps -aL | head -1 && ps -aL | greep mythread ; sleep1 ;echo"------";
done

在这里插入图片描述

线程分离

线程分离,分离之后的线程不需要被join,运行完毕之后,会自动释放Z,pcb
比喻 : 《同一个屋檐下的陌生人》
一个线程被设置detach以后,就不能够进行join了
主线程不退出,新线程处理业务完毕再退出
这边还需再研究
在这里插入图片描述

线程id的理解

为什么我们看到的线程id和ps -aL中的LWP不一致?

我们查看的到线程id是pthread库中的线程,不是Linux内核中的LWP, pthread库中的线程id是一个内存地址!!!
这个内存地址指的是虚拟地址
在这里插入图片描述
在这里插入图片描述

内核中的每一个LWP都对应一个线程控制块

在这里插入图片描述

在这里插入图片描述
线程崩溃的影响一定是有限的,因为它在进程内部,而进程是具有独立性的

3 .线程的同步和互斥

  • 因为多个线程是共享地址空间的,也就是很多资源都是共享的
    优点:通信方便
    缺点:缺乏访问控制
    因为一个线程的操作问题,给其他线程造成了不可控,或者引起崩溃,异常,逻辑不正确等的现象:线程安全
  • 创建一个函数没有线程安全的话,不要使用全局,stl ,new等会在全局内有效的数据(访问数据)

->为什么需要进行后续的同步和互斥

概念理解

  1. 临界资源:凡是被进程共享访问的资源都是临界资源(多线程,多进程打印数据到显示器,这里显示器就是临界资源)
  2. 临界区:我的代码中访问临界资源的代码(在我的代码中,不是所有1的代码都是进行访问临界资源的。而访问临界资源代码的区域我们称之为临界区)
  3. 对临界区进行保护的功能,本质:就是对临界资源的访问。方式:互斥或者同步
  4. 互斥:在任意时刻,只允许一个执行流访问某段代码(访问某部分资源),就可以称之为互斥!
  5. 比如:printf(”hello world“) ; -> lock(); printf(); unlock() ; -> 一个事情,要么不执行,要么就执行完毕,原子性(后续继续)
  6. 同步:一般而言,让访问临界资源的过程在安全的前提下(一般都是互斥and原子的),让访问资源具有一定的顺序性!这种顺序性具有合理性

抢票逻辑

usleep 以微秒位单位
1s == 1000ms 1ms = 1000us
ticket.cc如下

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

int tickets = 1000;

// 抢票逻辑、1000张票,5线程同时抢
// tickets就是所谓的临界资源
// 为了让多个进程进行切换,进程什么时候进行切换(1、时间片到了 2、检测的时候,从内核态到用户态的时候)
void* ThreadRun(void *args)
{
    int id = *(int*)args;
    delete (int*)args;

    while(true)
    {
        //邮票的时候
        if(tickets >0)
        {
            usleep(1000);
            std::cout<<"我是第["<<id<<"] ,我要抢的票是"<<tickets<<std::endl;
            printf("");
            tickets--;
        }
        else{
            //没有票的时候
            break;
        }
    }
}

int main()
{
    pthread_t tid[5];
    //id对应的是一个车票的编号
    for(int i = 0;i<5;i++)
    {
        int* id = new int(i);
        pthread_create(tid+i,nullptr,ThreadRun,id);
    }

    for(int i = 0;i<5;i++)
    {
        pthread_join(tid[i],nullptr);
    }

}   

现象:抢到了负数,实际购票的时候,不能有负数
在这里插入图片描述
变成负数的原因是在判断到进入代码的时候被切走了
这里的tickets是临界资源,真正访问的代码是if(tickets>0)那一段
在这里插入图片描述

#include<iostream>
#include<string>
#include<unistd.h>
#include<pthread.h>
#include<mutex> // C++11


class Ticket
{
    private:
    int tickets;
    pthread_mutex_t mtx; //原生线程库的接口
    std::mutex mtx1;

    public:
    Ticket()
    :tickets(1000)
    {
        pthread_mutex_init(&mtx,nullptr);
    }

    bool GetTicket()
    {
        //bool变量不被所有线程共享,保存在栈中
        bool res = true;
        
        //pthread_mutex_lock(&mtx);
        mtx1.lock(); //C++11用法

        //邮票的时候
        if(tickets> 0)
        {
            usleep(1000);
            std::cout<<"我是["<<pthread_self()<<"] ,我要抢的票是"<<tickets<<std::endl;
            tickets--;
            printf("");
        }
        else{
            //没有票的时候
            printf("票已经卖完了\n");
            res = false;
        }

        //pthread_mutex_unlock(&mtx);
        mtx1.unlock(); //C++11
        return res;
    }

    ~Ticket()
    {
        pthread_mutex_destroy(&mtx);
    }

};


// 抢票逻辑、1000张票,5线程同时抢
// tickets就是所谓的临界资源
// 为了让多个进程进行切换,进程什么时候进行切换(1、时间片到了 2、检测的时候,从内核态到用户态的时候)
void* ThreadRun(void *args)
{
    Ticket * t = (Ticket*)args;
    while(true)
    {
        if(!t->GetTicket())
        {
            break;
        }
    }

    
}

int main()
{
    Ticket *t = new Ticket();
    pthread_t tid[5];
    //id对应的是一个车票的编号
    for(int i = 0;i<5;i++)
    {
        pthread_create(tid+i,nullptr,ThreadRun,(void*)t);
    }

    for(int i = 0;i<5;i++)
    {
        pthread_join(tid[i],nullptr);
    }

    return 0;
}   

互斥锁

  • 我们可以把锁看成int lock = 1; if(lock)判断是否加锁,lock–就是加锁,lock+=1就是解锁,实际上lock的底层也就是这样的

    寄存器和寄存器内的数据是两码事,银行是被人民共享的,但是你里面的钱是属于你自己的
  • mutux的本质:其实是通过一条汇编,将锁的数据交换到自己的上下文中!!
  • 加锁的期间也是完全有可能被切走的,但是线程被切走的时候,上下文会被保护起来!而锁的数据也是再上下文中的。有锁被切走的线程,是“抱着锁”走的 ; 在此期间,其他线程休想申请锁,休想进去临界区!
  • 站在其他线程的视角,是不是对其他线程有意义的状态,是不是A线程要么就没有申请,要么就线程A使用完了锁 – 线程A访问临界区的原子性
  • 代码是程序员写的,为了保证临界区的安全,必须保证每个线程都必须遵守相同的“编码规范”(A申请锁,其他线程的代码也必须要申请)

第二种初始化的方式(静态/全局,这样就不需要写初始化函数和析构函数)
在这里插入图片描述

线程安全和可重入

在这里插入图片描述
结论:线程安全不一定是可重入的,可重入函数一定是线程安全的

死锁

多个进程或者一个进程申请一把自己永远申请不到的锁,导致自己被挂起了,这个进程/执行流就不能够被推进,这种情况就叫做死锁

例子:
同一把锁连续申请两次
第二次申请不成功被挂起了

在这里插入图片描述
在这里插入图片描述

条件变量

  • 一方可以告诉另一方事情已经就绪的场景
    在这里插入图片描述
  • 接口
    在这里插入图片描述
  • 核心函数
    在这里插入图片描述
#include<iostream>
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
pthread_cond_t cond;

pthread_mutex_t mtx;

//ctrl thread线程控制work线程,让他定期运行
void* ctrl(void* args)
{
    std::string name = (char*)args;

    while(true)
    {
        //唤醒条件变量下等待的一个线程,哪一个
        std::cout<<"master say : begin work"<<std::endl;
        pthread_cond_signal(&cond);
        sleep(1);
    }
}

void* work(void* args)
{
    int number =*(int*)args;
    delete (int*)args;

    while(true)
    {
        pthread_cond_wait(&cond,&mtx);
        std::cout<< "worker : "<<number << " is working ..." <<std::endl;
    }
}


int main()
{
    #define NUM 3

    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);

    pthread_t master;
    pthread_t worker[NUM];

    pthread_create(&master,nullptr,ctrl,(void*)"boss");//传地址 方法为默认 执行函数ctrl 启动函数参数

    for(int i = 0;i<NUM;i++)
    {
        int *number = new int(i);
        pthread_create(worker+i,nullptr,work,(void*)number);
    }

    for(int i = 0;i<NUM;i++)
    {
        pthread_join(worker[i],nullptr);//进行线程等待
    } 
    pthread_join(master,nullptr);

    pthread_mutex_destroy(&mtx);

    pthread_cond_destroy(&cond);

    return 0;

在这里插入图片描述

  • pthread_cond_broadcast一次唤醒所有
  • pthread_cond_signal 一次唤醒一个
    在这里插入图片描述
#include<iostream>
#include<cstdio>
#include<pthread.h>
#include<unistd.h>
pthread_cond_t cond;

pthread_mutex_t mtx;

//ctrl thread线程控制work线程,让他定期运行
void* ctrl(void* args)
{
    std::string name = (char*)args;

    while(true)
    {
        //唤醒条件变量下等待的一个线程,哪一个
        std::cout<<"master say : begin work"<<std::endl;
        //pthread_cond_signal(&cond);
        pthread_cond_broadcast(&cond);
        sleep(1);
    }
}

void* work(void* args)
{
    int number =*(int*)args;
    delete (int*)args;

    while(true)
    {
        std::cout<< "worker : "<<number << " is working ..." <<std::endl;
        pthread_cond_wait(&cond,&mtx);
    }
}


int main()
{
    #define NUM 3

    pthread_mutex_init(&mtx,nullptr);
    pthread_cond_init(&cond,nullptr);

    pthread_t master;
    pthread_t worker[NUM];

    pthread_create(&master,nullptr,ctrl,(void*)"boss");//传地址 方法为默认 执行函数ctrl 启动函数参数

    for(int i = 0;i<NUM;i++)
    {
        int *number = new int(i);
        pthread_create(worker+i,nullptr,work,(void*)number);
    }

    for(int i = 0;i<NUM;i++)
    {
        pthread_join(worker[i],nullptr);//进行线程等待
    } 
    pthread_join(master,nullptr);

    pthread_mutex_destroy(&mtx);

    pthread_cond_destroy(&cond);

    return 0;
}

在这里插入图片描述

4.生产消费模型

理解生产消费模型

  • 通过函数场景初步理解
    在这里插入图片描述
  • 超市模型理解解耦 - 消费者在购买东西的同时供应商一边在生产
    在这里插入图片描述
    帮助理解 – 《321模型》
    在这里插入图片描述

Blockingqueue

在这里插入图片描述

可能会出现的问题

  • if语句可能会出现以下的状况
    ->用while循环来避免
    在这里插入图片描述
  • 生产和消费,数据传输知识第一步,还需要进行任务处理

任务生产队列代码

BlockQueue.hpp
// hpp -> 开源文件使用->申明和定义放在一个文件中
#pragma once
#include <iostream>
#include <queue>
#include <pthread.h>

namespace ns_BlockQueue
{
    const int default_cap = 5;

    template <class T>
    class BlockQueue
    {
    private:
        std::queue<T> bq_;   //阻塞队列
        int cap_;            //阻塞队列的最大容量
        pthread_mutex_t mtx_; //保护临界资源的锁
        // 1.生产满的时候,就不需要再生产了(不需要竞争锁),就需要消费者来消费
        // 2.消费完的时候,就不会需要再消费了(不需要竞争锁了),就需要生产者来生产
        pthread_cond_t is_full_;  // bq_满的情况下,消费者在该条件变量下等待 ->消费者该s去消费了
        pthread_cond_t is_empty_; // bq_空的时候, 生产者在该条件变量下等待 ->生产者该去生产了
    private:
        bool isFull()
        {
            return bq_.size() == cap_;
        }
        bool isEmpty()
        {
            return bq_.size() == 0;
        }
        void LockQueue()
        {
            pthread_mutex_lock(&mtx_);
        }
        void UnlockQueue()
        {
            pthread_mutex_unlock(&mtx_);
        }
        void ProducterWait()
        {
            //调用pthread_cond_wait
            //首先第一步,会首先自动释放掉mtx_,然后再挂起自己(不会出现抱着锁被挂起产生死锁)
            //返回的时候,会自动竞争锁,获取到锁
            pthread_cond_wait(&is_empty_, &mtx_);
        }
        void ConsumerWait()
        {
            pthread_cond_wait(&is_full_,&mtx_);
        }
        void WakeupConsumer()
        {
            //怎么唤醒?
            //发信号,消费者是在is_full_这个条件变量中的
            pthread_cond_signal(&is_full_);
        }
        void WakeupProducter()
        {
            //同理唤醒生产者
            pthread_cond_signal(&is_empty_);
        }
    public:
        BlockQueue(int cap = default_cap) : cap_(default_cap)
        {
            pthread_mutex_init(&mtx_, nullptr);
            pthread_cond_init(&is_full_, nullptr);
            pthread_cond_init(&is_empty_, nullptr);
        }
        ~BlockQueue()
        {
            pthread_mutex_destroy(&mtx_);
            pthread_cond_destroy(&is_full_);
            pthread_cond_destroy(&is_empty_);
        }

    public:
        // const &:输入
        //*:输出
        //&:输入输出
        // bq_是临界资源,是stl库里面的东西,具有全局属性,我们要加锁来进行保护
        void Push(const T &in)
        {
            LockQueue();
            //临界区
            //无论是访问 还是 放数据 都是在访问临界资源,都要加锁进行保护
            //操作再lock和unlock之间是安全的
            // if (isFull())
            //我们需要进行条件检测的时候,这边需要使用循环方式
            //来保证退出循环一定是条件不满足导致的
            while(isFull())
            {
                //无法生产
                //等待的,把线程挂起,我们当前是持有锁的
                ProducterWait();
            }
            //向队列中放数据,生产函数s
            bq_.push(in);
            //唤醒消费者
            WakeupConsumer();
            UnlockQueue();
        }

        void Pop(T *out)
        {
            LockQueue();
            while(isEmpty())
            {
                //无法消费
                ConsumerWait();
            }
            //从队列中拿数据,消费函数
            *out = bq_.front();
            bq_.pop();
            //唤醒生产者
            WakeupProducter();
            UnlockQueue();
        }
    };
}
CpTest.cc
#include "BlockQueue.hpp"
#include "Task.hpp"

#include <time.h>
#include <cstdlib>
#include <unistd.h>

using namespace ns_BlockQueue;
using namespace ns_task;

void *consumer(void *args)
{
    //拿到阻塞队列
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    while (true)
    {
        // sleep(2);
        // int data = 0;
        // bq->Pop(&data); //消费者拿数据消费
        // std::cout<<"消费者消费了一个数据:"<<data<<std::endl;

        Task t;
        bq->Pop(&t); //任务消费的第一步
        // t.Run();
        t(); //任务消费的第二部
    }
}
void *productor(void *args)
{
    //拿到阻塞队列
    BlockQueue<Task> *bq = (BlockQueue<Task> *)args;
    std::string ops = "+-*/%"; //五个操作符
    while (true)
    {
        //制造数据
        // sleep(2);
        // int data = rand()%20 +1 ;//产生一个0到20的数
        // std::cout<<"生产者生产数据:"<<data<<std::endl;
        // bq->Push(data); //生产者产生数据

        // 1.制造数据,生产者的数据task从哪来
        int x = rand() % 20 + 1;   //[1,20]
        int y = rand() % 10 + 1;   //[1,10]
        char op = ops[rand() % 5]; //五个操作数分别对应下标 0 1 2 3 4 5
        // 2.派发任务
        Task t(x, y, op);
        std::cout << "生产者派发了一个任务" << x << op << y << "=?" << std::endl;
        bq->Push(t);
        // usleep(10000);
        // sleep(1);
    }
}
int main()
{
    //种一颗随机数种子
    srand((long long)time(nullptr));
    BlockQueue<Task> *bq = new BlockQueue<Task>();
    pthread_t c, p;           //生产者消费者两个线程
    pthread_t c1, c2, c3, c4; //有多个消费者

    //创建两个线程,传入阻塞队列
    pthread_create(&c, nullptr, consumer, (void *)bq);
    pthread_create(&c1, nullptr, consumer, (void *)bq);
    pthread_create(&c2, nullptr, consumer, (void *)bq);
    pthread_create(&c3, nullptr, consumer, (void *)bq);
    pthread_create(&c4, nullptr, consumer, (void *)bq);
    pthread_create(&p, nullptr, productor, (void *)bq);

    //让这两个线程进行等待
    pthread_join(c, nullptr);
    pthread_join(c1, nullptr);
    pthread_join(c2, nullptr);
    pthread_join(c3, nullptr);
    pthread_join(c4, nullptr);
    pthread_join(p, nullptr);
}
Task.hpp
#pragma once

#include <iostream>
#include <pthread.h>

namespace ns_task
{
    //这个类可以实现x,y两个数的+-*/%五种运算
    class Task
    {
    private:
        int x_;
        int y_;
        char op_;

    public:
        Task() {}
        Task(int x, int y, char op) : x_(x), y_(y), op_(op)
        {
        }
        ~Task() {}
        int Run()
        {
            int res = 0;
            switch (op_)
            {
            case '+':
                res = x_ + y_;
                break;
            case '-':
                res = x_ - y_;
                break;
            case '*':
                res = x_ * y_;
                break;
            case '/':
                res = x_ / y_;
                break;
            case '%':
                res = x_ % y_;
                break;
            default:
                std::cout << "这里出现了bug..." << std::endl;
                break;
            }
            std::cout << "当前任务正在被" << pthread_self() << "处理"
                      << x_ << op_ << y_ << "=" << res << std::endl;
            return res;
        }

        int operator()()
        {
            return Run();
        }
    };
}
Makefile
Cptest:CpTest.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f Cptest

5.POSIEX信号量

回顾信号量概念

  • 信号量本质是一把计数器,描述临界资源中资源数目的大小!(最多能有多少资源分配给线程)
  • 每个线程都想访问临界资源,都得先申请访问信号量资源
  • 买票的本质:预定资源
  • 多线程预定资源的手段:临界资源如果可以被分成一个一个的小资源,如果处理得当,我们也可以让多个线程同时访问临界资源的不同区域,从而实现并发

认识信号量对应的操作函数

  • P(passeren通过) V(vrijgeven释放)
    在这里插入图片描述

    在这里插入图片描述
  • PV操作
  • P(申请资源)
  • V(释放资源)
    在这里插入图片描述

认识一个环形队列

  • 我们的环形结构使用数组通过膜运算来模拟环形结构
    在这里插入图片描述
1.基本原理和思想
  • 生产者最关心的是 环形队列中空的位置
  • 消费者最关心的是 环形队列中的数据
  • 规则1:生产者不能把消费者套成一个圈
  • 规则2:消费者不能超过生产者
  • 规则3:当指向同一个位置的时候,要根据空,满的状态,来判定让谁先执行1
  • 其他:除此以外,生产和消费可以并发执行
    在这里插入图片描述
2.伪代码

生产者空的时候进行生产,释放后,消费者进行消费,消费者消费完了,释放后又有空位置了,循环往复
在这里插入图片描述

结合sem + 环形队列编写生产消费模型

ring.cc
#include "ring_queue.hpp"
#include <pthread.h>
#include <time.h>
#include <unistd.h>

using namespace ns_ring_queue;

void *consumer(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    //消费一个数据
    while (true)
    {
        int data = 0;
        rq->Pop(&data);
        std::cout << "消费了一个数据,这个数据是:" << data << std::endl;
        //sleep(1);
    }
}

void *productor(void *args)
{
    RingQueue<int> *rq = (RingQueue<int> *)args;
    //生产一个数据
    while (true)
    {
        int data = rand() % 20 + 1;
        std::cout << "生产的数据是:" << data << std::endl;
        rq->Push(data);
    }
}

int main()
{
    srand((long long)time(nullptr));
    //两种关系 生产者 -- 消费者
    RingQueue<int> *rq = new RingQueue<int>();
    pthread_t c, p;

    pthread_create(&c, nullptr, consumer, (void *)rq);
    pthread_create(&p, nullptr, productor, (void *)rq);

    pthread_join(c, nullptr);
    pthread_join(p, nullptr);

    return 0;
}
ring_queue.hpp
#pragma once

#include <iostream>
#include <vector>
#include <semaphore.h>

namespace ns_ring_queue
{
    //环形队列中位置的数量
    const int g_cap_default = 10;

    template <class T>
    class RingQueue
    {
    private:
        std::vector<T> ring_queue_;
        int cap_;
        //生产者关心空位置资源
        sem_t blank_sem_;
        //消费者关心空位置资源
        sem_t data_sem_;
        int p_step_;
        int c_step_;

    public:
        RingQueue(int cap = g_cap_default):ring_queue_(cap), cap_(cap)
        {
            sem_init(&blank_sem_, 0, cap);
            sem_init(&data_sem_, 0, 0);
            p_step_ = c_step_ = 0;
        }
        ~RingQueue()
        {
            sem_destroy(&blank_sem_);
            sem_destroy(&data_sem_);
        }

    public:
        void Pop(T *out)
        {
            //消费接口
            sem_wait(&data_sem_); //p操作 申请一个数据位置 资源
            *out = ring_queue_[c_step_];
            sem_post(&blank_sem_);//v操作 释放一个空位置资源
            c_step_++;
            c_step_%=cap_;
        }
        //目前最高优先级的先实现单生产和单消费
        void Push(const T &in)
        {
            //生产接口
            sem_wait(&blank_sem_); // P(空位置) ->申请空位置资源
            ring_queue_[p_step_] = in;
            sem_post(&data_sem_); // V(数据) ->释放一个数据的位置的资源
            //可以生产了,但是往哪个位置生产呢?
            p_step_++;
            p_step_ %= cap_;
        }
    };
}
Makefile
ring_cp:ring_cp.cc
	g++ -o $@ $^ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f ring_cp

6.线程池

池的概念

  • 内存池举例
  • 内存池的核心目的就是提高效率
    在这里插入图片描述
  • 提前准备一批线程,用来随时准备任务就是线程池
  • 线程池的创建,也是为了提高效率 在这里插入图片描述

简易线程池的设计

竞争着要任务,竞争着给线程
在这里插入图片描述
查看结果
在这里插入图片描述
线程被killed掉?
OS把它给杀掉了,起太多的线程,在进行疯狂的操作

7.单例模式

为什么要了解?

常用 && 经典

什么是设计模式?

一些固定的套路,可以写出高质量代码,大佬根据经典常见给出解决方式,编程经验的高度总结

单例模式的特点
  • 某些类只应该具有一个对象(实例),就称之为单例
  • 在很多服务器开发场景中,经常需要让服务器加载很多数据(上百G)到内存中,此时往往要用一个单例的类来管理这些数据
  • 动态库也只需要一份
饿汉模式和懒汉模式

在这里插入图片描述

  • 例子
  • 饿汉方式:吃完饭立即洗碗
  • 懒汉方式:吃完饭等准备吃下顿饭再洗碗
  • 懒汉核心思想: ”延时加载“

饿汉模式实现单例

static修饰的变量是属于类而不属于对象的
静态成员初始化一定是在类外做初始化
在这里插入图片描述

懒汉方式实现单例模式

在这里插入图片描述

懒汉方式实现单例模式(线程安全版本)

一个线程池就够了
实现单例模式版的线程池

8.读者写者模型

介绍

  • 适用场景
    1.对数据,大部分的操作是读取,少量的操作是写入
    2.判断依据是,进行数据读取(消费)的一段,是否会将数据取走,如果不取走,就可以考虑读者写者模型
  • 321原则
  • 3种关系
    读者和读者
    写者和写者
    读者和写者
  • 两种角色
    读者和写者
  • 一个交易场所
    一段缓冲区(自己申请的,还是stl)
  • 生活中适用:
    博客,新闻

在这里插入图片描述

接口

在这里插入图片描述

如何理解?

下面是一段伪代码

  • 维护了写者和写者的互斥,读者和写者的互斥
  • 互斥锁+条件变量就能完成
  • 读者优先:读者和写者同时到来的时候,我们让读者先访问
  • 写者优先:当读者和写者同时到来的时候,比当前写者晚来的所有的读者,都不要进入临界区访问了,等临界区中没有读者的时候,让写者先写入
    在这里插入图片描述
    在这里插入图片描述

9.挂起等待特性锁 vs 自旋锁

  • 将线程挂起等待是有成本的
    如果花费的时间非常短?- 比较适合自旋锁
    如果花费的时间非常长? - 比较适合挂起等待锁
  • 自旋锁的本质:不断的通过循环检测锁的状态
    在这里插入图片描述
  • 李四和张三的例子
    在这里插入图片描述
  • 接口
    在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值