Linux多线程_(线程池,读者写者,自旋锁)

Posted LHlucky_2

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux多线程_(线程池,读者写者,自旋锁)相关的知识,希望对你有一定的参考价值。

1.线程池概念

  • 一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。

2.线程池概念图

在这里插入图片描述

  • 没有线程池好比银行业务窗口只有一个,那么人越多,等待时间越长,效率越低。内核就好比单一窗口里面的服务人员,长时间大量处理业务,难免会出现问题。线程池的概念就好比多开放几个窗口,来节省时间,提高效率。
  • 在服务器接收来自用户发来的任务时,需要创建线程去处理任务,而这都需要花时间为代价,如果任务队列接受一个任务创建一个线程的去处理任务,那么在面对大量任务的同时,对用户来说,需要等待很长时间,效率很低,对内核来说,需要频繁的内核申请,创建线程,销毁线程。造成内核过度调用,降低性能与效率。

3.线程池的应用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用

4.线程池的种类

  1. 创建固定数量线程池,循环从任务队列中获取任务对象,
  2. 获取到任务对象后,执行任务对象中的任务接口

5.代码示例

//ThreadPool.hpp

#pragma once
#include<iostream>
#include<queue>
#include<math.h>
#include<unistd.h>

#define NUM 5 //线程数量

class Task //任务(求一个数的平方)
{
  private:
    int base;
  public:
    Task()
    {}
    Task(int _b):base(_b)
    {}
    void Run()
    {
      std::cout<< "pthread id:["<< pthread_self()<<"] "<<base<< ":" <<"pow is: "<< pow(base,2) <<std::endl;
    }
    ~Task()
    {}
};

class ThreadPool 
{
  private:
    std::queue<Task*> q; //任务队列
    int max_num;  //线程数量
    pthread_t *t; //线程数组
    pthread_mutex_t lock; //互斥锁
    pthread_cond_t cond;//只有消费者需要环境变量,生产者不需要,如果需要说明环形队列任务已满。
  public:
    void LockQueue() 
    {
      pthread_mutex_lock(&lock);
    }
    bool Isempty()
    {
      return q.size()==0;
    }
    void ThreadWait()
    {
      pthread_cond_wait(&cond,&lock);
    }
    void UnLockQueue()
    {
      pthread_mutex_unlock(&lock);
    }
    void ThreadWakeup()
    {
      //pthread_cond_signal(&cond);
      pthread_cond_broadcast(&cond);
    }
  public:
    ThreadPool(int _max = NUM):max_num(_max)
    {}
    static void *Rountine(void* arg)
    {
      ThreadPool *tp = (ThreadPool*)arg;
      while(1)
      { 
        sleep(2);
        tp->LockQueue(); // 给任务队列上锁,保持互斥
        while(tp->Isempty())
        {
          tp->ThreadWait(); //如果为空则挂起(没有任务线程无序处理等待任务到来)
        }
        Task t;  
        tp->Get(t); // 有任务并拿走任务
        tp->UnLockQueue(); // 解锁
        t.Run(); // 处理任务
      }
      sleep(2);
    }
    void ThreadPoolInit()
    {
        pthread_mutex_init(&lock,nullptr); //初始化锁
        pthread_cond_init(&cond,nullptr); //初始化环境变量
        pthread_t *t=new pthread_t[max_num]; // 创建线程数组
        for(int i=0;i<max_num;i++)
        {
            pthread_create(t+i,nullptr,Rountine,this); //创建线程
        }
    }
    void Put(Task &in) // 给任务队列里面放任务
    {
        LockQueue();
        q.push(&in);
        UnLockQueue();
        
        ThreadWakeup();
    }

    void Get(Task &out) //拿任务
    {
        Task *t = q.front();
        q.pop();
        out = *t;
    }
    ~ThreadPool()
    {
      pthread_mutex_destroy(&lock); //销毁锁
      pthread_cond_destroy(&cond); //销毁环境变量
      delete []t; //销毁数组
    }
};

//main.cpp

#include "ThreadPool.hpp"

int main()
{
  ThreadPool *tp = new ThreadPool(5); //创建线程池
  tp->ThreadPoolInit(); //初始换线程池

  while(1) //主线程创建任务
  {
    int x=rand()%10 + 1;  //计算1到10之间随机数的平方
    Task t(x);
    tp->Put(t); //将任务放进任务队列
    sleep(1);
  }
  return 0;
}

5.1 运行结果

5.1.1 只唤醒单个线程

在这里插入图片描述

5.1.2 一次唤醒所有线程

在这里插入图片描述

  • 惊群效应
  • 惊群效应(thundering herd)是指多进程(多线程)在同时阻塞等待同一个事件的时候(休眠状态),如果等待的这个事件发生,那么他就会唤醒等待的所有进程(或者线程),但是最终却只能有一个进程(线程)获得这个时间的“控制权”,对该事件进行处理,而其他进程(线程)获取“控制权”失败,只能重新进入休眠状态,这种现象和性能浪费就叫做惊群效应

6.线程池存在价值

  1. 有任务,立马有线程进行服务,省掉了线程创建的时间。
  2. 有效防止服务器中线程过多,导致系统过载的问题。

6.1 线程池 VS 进程池

  1. 线程池占用资源更少,但是健壮性(鲁棒性)不强。
  2. 进程池占用资源更多,但是健壮性(鲁棒性)很强。

7. 线程安全的单例模式

7.1 什么是单例模式

  • 单例模式是一种 “经典的, 常用的, 常考的” 设计模式

7.2 什么是设计模式

  • IT行业这么火, 涌入的人很多. 俗话说林子大了啥鸟都有. 大佬和菜鸡们两极分化的越来越严重. 为了让菜鸡们不太拖大佬的后腿, 于是大佬们针对一些经典的常见的场景, 给定了一些对应的解决方案, 这个就是 设计模式。

7.4 单例模式的特点

  • 某些类, 只应该具有一个对象(实例), 就称之为单例,例如一个男人只能有一个媳妇。
  • 在很多服务器开发场景中, 经常需要让服务器加载很多的数据 (上百G) 到内存中. 此时往往要用一个单例的类来管理这些数据。

8. 其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
  • 自旋锁,公平锁,非公平锁

9. 读者写者问题

  • 在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
    在这里插入图片描述
  • 注意:写独占,读共享,读锁优先级高

9.1 生产者消费者 VS 读者写者

读者写者

  • 读者读者之间是共享关系。读者写者之间是互斥同步关系。写者之间互斥关系
  • 生产者消费者VS读者写者:消费者会取走数据,而读者不会,只看到数据就行。

9.2 读写锁接口

读写锁的初始化与销毁
在这里插入图片描述

  • 参数rwlock表示的是一个读写锁,attr是读写锁的属性,一般设置为NULL;

读加锁
在这里插入图片描述
写加锁
在这里插入图片描述

  • rdlock若申请不到锁,则自旋,tryrdlock若申请不到锁,则返回,由用户自旋;

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

10.自旋锁

在这里插入图片描述
自旋锁的类型

  • 普通自旋锁
  • 读写自旋锁
  • big-reader自旋锁

10.1 自旋锁接口

  • 初始化以及销毁锁
    在这里插入图片描述
  • 上锁
    在这里插入图片描述
  • 解锁
    在这里插入图片描述

10.2 自旋锁缺点

  • 如果线程执行的任务需要非常长的时间,或者线程对共享数据的竞争相当激烈,那么使用自旋锁的效率就很低。因为自旋的过程中,一直无法获取到锁,一直在白白的浪费CPU的资源。
  • 可能引起死锁
  • 过多的占用CPU资源

10.3 自旋锁与互斥锁的异同:

  • 自旋锁与互斥锁都是为对临界资源进行保护而创造的一种锁机制。不同之处在于,执行单元在持有互斥锁期间,其它需要该资源的执行单元是要进入睡眠状态的。而执行单元持有自旋锁期间,其它需要该资源的执行单元是进行不断的尝试的,直到持有自旋锁的单元释放自旋锁,则可以获得该资源。自旋锁这个名字也是由此得来。而且,由于需要获得该锁的单元不断进行尝试,所以,自旋锁的效率是远高于互斥锁的。

以上是关于Linux多线程_(线程池,读者写者,自旋锁)的主要内容,如果未能解决你的问题,请参考以下文章

LINUX多线程(线程池,单例模式,线程安全,读者写者模型)

多线程编程之读写锁

线程同步之读写锁(锁操作的补充)

线程同步互斥锁和读写锁的区别和各自适用场景

多线程面试题系列(14):读者写者问题继 读写锁SRWLock

Linux----多线程(下)