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

Posted 西科陈冠希

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LINUX多线程(线程池,单例模式,线程安全,读者写者模型)相关的知识,希望对你有一定的参考价值。

线程池,单例模式,线程安全,读者写者模型

线程池

基本概念

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

使用场景

  1. 需要大量的线程来完成任务,且完成任务的时间比较短。 WEB服务器完成网页请求这样的任务,使用线程池技术是非常合适的。因为单个任务小,而任务数量巨大,你可以想象一个热门网站的点击次数。 但对于于长时间的任务,比如一个Telnet连接请求,线程池的优点就不明显了。因为Telnet会话时间比线程的创建时间大多了。
  2. 对性能要求苛刻的应用,比如要求服务器迅速响应客户请求。
  3. 接受突发性的大量请求,但不至于使服务器因此产生大量线程的应用。突发性大量客户请求,在没有线程池情况下,将产生大量线程,虽然理论上大部分操作系统线程数目最大值不是问题,短时间内产生大量线程可能使内存到达极限,出现错误.

图解实例


左边的云朵状的我们称之为任务,当我们有需求给服务器传输过去时候,
没有线程池:

服务器回去创建线程。

有线程池:

直接调用线程来执行。
这样线程池的有点就体现李一点,就是省去了创建和销毁的时间。

正常情况下,如果一直去执行那么会浪费线程资源,于是在服务器和线程池中就存在一个任务队列,用户将任务给服务器,服务器给任务队列,线程池通过任务队列来拿数据进行处理,这样就不需要过多的浪费线程池资源。

价值体现

  1. 有任务,立马有线程进行服务,省掉了线程创建的时间。
  2. 有效防止,servers中线程过多,导致系统过载的问题。
  3. 线程池占用资源少,健壮性不强,进程池则相反。

模拟实现线程池

class Task
  public:
    int base;
  public:
    Task()
    Task(int _b):base(_b)

    void Run()
    
      std::cout <<"thread is[" << pthread_self() << "] task run ... done: base# "<< base << " pow is# "<< pow(base,2) << std::endl;
    
    ~Task()
    
;

class ThreadPool
  private:
    std::queue<Task*>q;
    int max_num;
    pthread_mutex_t lock;
    pthread_cond_t cond;
    bool quit;
  public:
    void LockQueue()
    
      pthread_mutex_lock(&lock);
    
    void UnlockQueue()
    
      pthread_mutex_unlock(&lock);
    
    bool IsEmpty()
    
      return q.size()==0;
    
    void ThreadWait()
    
      pthread_cond_wait(&cond,&lock);
    
    void ThreadWake()
    
      pthread_cond_signal(&cond);
    
  public:
    ThreadPool(int _max=NUM):max_num(_max)
    
    static void *Routine(void *arg)
    
      ThreadPool *this_p=(ThreadPool*)arg;
      while(true)
      
        this_p->LockQueue();
        while(true&&this_p->IsEmpty())
        
          this_p->ThreadWait();
        
        Task t;
        if(true&&!this_p->IsEmpty())
        
          this_p->Get(t);
        
        this_p->UnlockQueue();
        t.Run();
      
    
    void ThreadPoolInit()
    
      pthread_mutex_init(&lock, nullptr);
      pthread_cond_init(&cond, nullptr);
      pthread_t t;
      for(int i = 0; i < max_num; i++)
        pthread_create(&t, nullptr, Routine, this);                          
      
    
    void Put(Task&in)
    
      LockQueue();
      q.push(&in);
      UnlockQueue();
      ThreadWake();
    

    void Get(Task&out)
    
      Task* t=q.front();
      q.pop();
      out=*t;
    
    void ThreadQuit()
    
      if(!IsEmpty())
      
        std::cout<<"empty"<<std::endl;
        return;
      
      quit=true;
      ThreadWake();
    
    ~ThreadPool()
    
      pthread_mutex_destroy(&lock);
      pthread_cond_destroy(&cond);
    

;

线程安全的单例模式

基本概念

什么是单例模式

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

什么是设计模式

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

饿汉实现方式和懒汉实现方式

当我们进入游戏的时候前期的加载工作就是饿汉,因为不可能将所有需要的文件第一次载入,因为时间很长,所以后边就会实现懒汉的方式再你需要使用的时候,在进行加载。

饿汉方式实现单例模式

emplate <typename T>
class Singleton 
static T data;
public:
static T* GetInstance() 
return &data;

;

就是再创建的时候就已经实例化,下次使用直接使用。

懒汉方式实现单例模式

template <typename T>
class Singleton 
static T* inst;
public:
static T* GetInstance() 
if (inst == NULL) 
inst = new T();

return inst;

;

在需要使用的时候再实例化。

STL,智能指针和线程安全

不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响.

而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).

因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.

智能指针是否是线程安全的

对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.

对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.

其他常见的各种锁

  • 悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。

  • 乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。

  • CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试。

读者写者问题

基本概念

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

读者写者分析

在这句话中包含着几层关系:
读者和读者之间:共享同一个资源
写者和写者之间:互斥
写者和读者之间:互斥,同步

和生产消费不一样的是,消费者会取走数据而读者不会

伪代码分析

读优先

pthread_rwlock_rdlock()
P(r)
rc=rc+1;
if(rc==1)

P(w);

V(r);
读取数据
P(r)
rc=rc-1;
if(rc==0)

V(w);

V(r);

这里先申请读锁,然后对齐进行P操作将读者+1,如果已经有读者了就不让写者进入了P(w),出去的时候释放V(r)。

读取数据的时候,先P操作申请,知道最后一个读者出去rc==0,将V(w),此时写者就能进入,然后V(r)操作。

以上是关于LINUX多线程(线程池,单例模式,线程安全,读者写者模型)的主要内容,如果未能解决你的问题,请参考以下文章

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

Linux线程池 | 线程安全的单例模式 | STL智能指针与线程安全 | 读者写者问题

Linux----多线程(下)

Linux操作系统多线程

Linux操作系统多线程

Linux__线程池及设计模式(单例模式)