Linux 中的多线程

1 Linux 线程控制

在上一篇文章里提高,Linux 并没有像 win 那样真正意义上的线程,而是用进程去模拟线程的,所以 Linux 中的线程创建等一系列的操作由 NPTL POSIX线程库实现。

  • 与线程有关的函数构成了一个完整的系列,绝大多数函数的名字都是以"pthread_"打头的
  • 要使用这些函数库,要通过引入头文<pthread.h>
  • 链接这些线程函数库时要使用编译器命令的"-lpthread"选项

在这里插入图片描述

1.1 创建线程

接口介绍

在这里插入图片描述

  • 功能:创建一个新的进程
  • 参数thread: 返回线程ID,attr:设置线程的属性,attr 为 NULL 表示使用默认属性,start_routine 是函数指针,指向线程启动后要执行的函数代码块,arg : 传给线程启动函数的参数
  • 返回值:成功返回0,失败返回错误码

代码演示

在这里插入图片描述

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

线程ID及进程地址空间布局

  • pthread_create 函数会产生一个线程ID(tid),存放在第一个参数指向的地址中。该线程ID,和LWP并不是一个
  • 前面所说的LWP,是属于线程调度的范畴。因为线程是轻量级进程,是操作系统调度的最小单位,所以需要一个数值来唯一表示该进程。
  • phread_create 函数第一个参数指向的虚拟内存单元,该内存单元的地址就是新创建线程的线程ID,属于NPTL线程库的范畴。线程库的后序操作,就是根据该线程ID来操作线程的,前面的代码和后面的代码都有所体现。

获得线程ID的接口:pthread_self()
在这里插入图片描述

1.2 线程终止和线程等待

  1. main 函数所在的线程是主线程,主线程结束退出,则线程所在的进程就会结束,则其他线程也会随之退出,因为进程是承担系统资源分配的基本单位,进程退出了,那么基于这个进程资源所创建的进程肯定就没有了。
  2. 那么新线程终止 了,主线程如何知道你已经终止了呢,这就需要线程等待,如果没有线程等待,也会发生像僵尸进程那样的问题,造成资源泄露,主线程在进行等待的时候会阻塞,还有一种方式叫线程分离也可以解决这个问题。
  3. 线程出现异常会导致线程所在的进程退出,那么处理这个情况就是进程的问题,所以我们默认线程退出只有两个情况1、代码跑完结果正确 2、代码跑完结果不正确
  4. 信号是专门为进程设计的,信号处理的基本单位是进程,所以block表(信号屏蔽字)是线程私有的,但是 pending(未决表)是进程私有的

1.2.1 线程终止的方式

注意:在线程中调用 exit 也是终止该进程所在的进程。想要单独终止进程有3种方式

  • 从线程函数 return 。这个方法对主线程不适用,从 main 函数 return 相当于调用 exit。
  • 线程可以调用 pthread_exit 来终止自己
  • 一个线程可以调用 pthread_cancel 来 终止同一进程中的另一个线程。

pthread_exit介绍

在这里插入图片描述

需要注意,pthread_exit或者return返回的指针所指向的内存单元必须是全局的或者是用malloc分配的,不能在线程函数的栈上分配,因为当其它线程得到这个返回指针时线程函数已经退出了。

pthread_cancel

在这里插入图片描述
代码演示
在这里插入图片描述
结果
在这里插入图片描述

在这里插入图片描述

1.2.2 线程等待的方式

为什么需要线程等待呢?

已经退出的线程,其空间没有被释放,仍然在该进程的地址空间内。
创建的新进程不会复用新进程的地址空间,造成资源泄露。

函数接口介绍 pthread_join

在这里插入图片描述

  • 参数 thread:线程ID(tid) retval :它指向一个指针,后者指向线程的返回值
  • 返回值:成功返回 0 ,失败返回错误码。

1.2.3 小结

  • 如果 thread 线程通过 return 返回 ,retval 所指向的单元存放的是thread线程函数的返回值。
  • 如果 thread 线程被别的线程调用 pthread_cancel终止的,retval 所指向的单元存放的是常数 PATHREAD_CANCELED(-1)
  • 如果 thread 线程是自己调用 pthread_exit 终止的,retval 所指向的单元存放的是传给 pthread_exit的参数。
  • 如果对 thread 线程的终止状态不感兴趣,可以传NULL给value_ ptr参数。

1.3 线程分离

默认情况下,新创建的线程是需要被等待的,新线程退出后,需要对其进行 pthread_join 操作,否则无法释放资源,从而造成系统泄露。
如果不关心线程的返回值,join是一种负担,这个时候我们可以告诉系统,当先线程退出的时候,自动释放线程的资源,需要进行线程分离。

接口 int pthread_detach(pthread_t thread)

  • 可以是线程组内其他线程对目标线程进行分离,也可以是线程自己分离
  • joinable 和 分离是冲突的,一个线程不能既是 joinable 又是分离的。

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

2 Linux 线程互斥(重点)

2.1 进程、线程间的互斥相关背景概念

  • 临界资源:多个线程执行流共享的资源叫做临界资源(多个线程可能会同时访问的资源)
  • 临界区:每个线程内部,访问临界资源的代码,就叫做临界区
  • 互斥:任何时刻,互斥保证有且只有一个执行流进入进入临界区,访问临界资源,对临界资源起保护作用。
  • 原子性:不被任何调度机制打断的操作,该操作只有两态,要么完成要么未完成

1、所有线程都必须遵守:对临界区进行保护
2、lock(加锁) ===> 访问临界区 ==> unlock(解锁)
3、所有的线程都必须先看到同一把锁,锁本身也是临界资源,申请锁的过程也是两态的,即lock具有原子性,unlock 也具有原子性。
4、lock > 访问临界区(占用一定的时间处理)
=> unlock ,在特定线程或者进程拥有锁的时候,期间有新线程来申请锁,一定是申请不到的!那个新线程将阻塞,将新线程/进程 对应的 PCB 投入到等待队列中,特定的 线程或者进程 unlock 之后,进行线程或进程的唤醒操作!
5、一次保证只有一个线程进入临界区,访问临界资源,就叫做互斥。

2.2 互斥量 mutex

我们看一个简单的抢票代码:4个线程抢票
在这里插入图片描述
在这里插入图片描述
结果却出现了,车票代码为负数的情况
在这里插入图片描述
解析原因

  • 大部分情况线程使用的数据都是局部变量,变量的地址空间在线程栈空间内,这种情况,变量归属单个进程,其他线程无法获得这种变量。

  • 但是有时候许多变量需要在线程间共享,这样的变量称为共享变量,通过数据的共享完成线程之间的交互。

  • 多个线程并发的操作共享变量,会带来一些问题,比如上面的抢票代码。

  • 那么具体到这个抢票代码的问题,我们来分析一下:

  • if 语句判断条件为真以后,代码可以并发的切换到其他线程

  • usleep 这个模拟漫长业务的过程,在这个漫长的业务过程中,可能有很多个线程会进入该代码段

  • –ticket 操作本身就不是一个原子操作

在这里插入图片描述

  • 前置–操作并不是原子操作,而是对应三条汇编指令:
  • load :将共享变量ticket从内存加载到寄存器中 update : 更新寄存器里面的值,执行-1操作 store :将新值,从寄存器写回共享变量ticket的内存地址

要解决以上问题需要做到以下3点

  • 代码必须要有互斥行为:当代码进入临界区执行时,不允许其他线程进入该临界区。
  • 如果多个线程同时要求执行临界区的代码,并且临界区没有线程在执行,那么只能允许一个线程进入该临界区。
  • 如果线程不在临界区中执行,那么该线程不能阻止其他线程进入临界区。

要做到这三点,本质上就是需要一把锁。Linux上提供的这把锁叫互斥量。

在这里插入图片描述
关于互斥量的接口
1、初始化与销毁互斥量
在这里插入图片描述
需要注意的地方

  • 使用 PTHREAD_ MUTEX_ INITIALIZER 初始化的互斥量不需要销毁
  • 不要销毁一个已经加锁的互斥量
  • 已经销毁的互斥量,要确保后面不会有线程再尝试加锁

2、互斥量加锁与解锁
在这里插入图片描述
需要注意的地方

  • 返回值:成功返回 0,失败返回错误码
  • 互斥量处于未锁状态,该函数会将互斥量锁定,同时返回成功
  • 发起函数调用时,其他线程已经锁定互斥量,或者存在其他线程同时申请互斥量,但没有竞争到互斥量,那么pthread_lock调用会陷入阻塞(执行流被挂起),等待互斥量解锁。

用 mutex 互斥量优化上面的抢票代码

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

3 可重入和线程安全

3.1 概念

  • 线程安全:多个线程并发同一段代码时,不会出现不同的结果。
  • 重入:同一个函数被不同的执行流调用,当前一个流程还没有执行完,就有其他的执行流再次进入,我们称之为重入。一个函数在重入的情况下,运行结果不会出现任何不同或者任何问题,则该函数被称为可重入函数,否则,是不可重入函数

3.2 常见的线程不安全的情况

  • 不保护共享变量的函数
  • 函数状态随着被调用,状态发生变化的函数
  • 返回指向静态变量指针的函数
  • 调用线程不安全函数的函数

3.3 常见的线程安全的情况

  • 每个线程对全局变量或者静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
  • 类或者接口对于线程来说都是原子操作
  • 多个线程之间的切换不会导致该接口的执行结果存在二义性

3.4 常见不可重入的情况

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

3.5 常见可重入的情况

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

3.6 可重入与线程安全的联系和区别

联系

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

区别

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

4 常见的锁概念

4.1 什么是死锁

  • 死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

4.2 产生死锁的4个必要条件

  • 互斥条件:一个资源每次只能被一个执行流使用
  • 请求与保持条件:一个执行流因请求资源而阻塞时,并且对已获得的资源保持不放
  • 不剥夺条件:一个执行流已获得的资源,在末使用完之前,不能强行剥夺
  • 循环等待条件:若干执行流之间形成一种头尾相接的循环等待资源的关系

4.3 如何避免死锁

  • 破坏死锁的四个必要条件
  • 加锁顺序一致
  • 避免锁未释放的场景
  • 资源一次性分配

避免死锁的算法

  • 死锁检测算法
  • 银行家算法

5 Linux 线程同步(重点)

5.1 什么是线程同步

例如,A 线程访问队列时,发现队列为空,它只能等待,直到 B 线程将一个节点添加到队列中,此时需要线程同步。需要条件变量。那么线程同步的定义:在保证数据安全的前提下(加锁),让多个执行流(线程)按照某种特定的顺序访问临界资源,从而有效避免饥饿问题,叫做同步

5.2 为什么需要线程同步

需要多线程协同高效的完成任务

5.3 如何编码实现

1、如果条件不满足,等待,释放锁。
2、通知机制
使用一组接口

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
简单的使用演示

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

6 生产者消费者模型

通过本章,我们要知道,什么是生产者消费者模型,为什么会存在这种模型,这种模型该如何设计并编码,并通过一个基于阻塞队列的生产者消费者模型,阐述 pthread_cond_wait,为何需要互斥量,和条件变量的规范使用。

6.1 什么是生产者消费者模型

  • 该模型有两个角色:生产者、消费者,维护了3个关系:生产者和生产者之间的互斥挂关系,生产者和消费者之间的同步关系、消费者和消费者之间的互斥关系,实现这样的模型可以代码的解耦,一旦解耦代码的可维护性强,同时适配了生产和消费速度不一致的问题,提高效率(321原则)。

6.2 为什么需要生产者消费者模型

  • 生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。这个阻塞队列就是用来给生产者和消费者解耦的。

6.3 生产者消费者模型的优点

  • 解耦
  • 支持并发
  • 解决忙闲不均

在这里插入图片描述

6.4 基于BlockingQueue的生产者消费者模型的代码实现

  • 介绍:在多线程编程中阻塞队列(Blocking Queue)是一种常用于实现生产者和消费者模型的数据结构。其与普通的队列区别在于,当队列为空时,从队列获取元素的操作将会被阻塞,直到队列中被放入了元素;当队列满时,往队列里存放元素的操作也会被阻塞,直到有元素被从队列中取出(以上的操作都是基于不同的线程来说的。

在这里插入图片描述
用 C++ queue 模拟阻塞队列的生产消费模型

  • 版本一:生产 int 数据 消费 int 数据,单消费者、单生产者

Makefile
在这里插入图片描述

BlockQueue.hpp
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

main.cc
在这里插入图片描述
在这里插入图片描述
结果
在这里插入图片描述

  • 版本2 :多消费者,多生产者,生产和消费任务 Task

BlockQueue.hpp

#pragma once
#include <iostream>
#include <pthread.h>
#include<queue>

//封装的任务
class Task {
public:
       int _x;
       int _y;
public:
       Task(){}
       Task(int x, int y):_x(x),_y(y)
       {
          
       }
       int Run()
       {
         return _x + _y;
       }
};

template<class T>
class BlockQueue {
private:
       std::queue<T> _q;
       size_t _cap;
       pthread_mutex_t lock;
       pthread_cond_t  p_cond;
       pthread_cond_t  c_cond;
private:
       void LockQueue()
       {
          pthread_mutex_lock(&lock); 
       }
       void UnlockQueue()
       {
          pthread_mutex_unlock(&lock);
       }
       void ProductorWait()
       {
          std::cout << "productor wait ..." << std::endl;
          pthread_cond_wait(&p_cond, &lock);
       }
       void ConsumerWait()
       {
          std::cout << "consumer wait ..." << std::endl;
          pthread_cond_wait(&c_cond, &lock);
       }
       void WakeupProductor()
       {
          std::cout << "wake up productor ..." << std::endl;
          pthread_cond_signal(&p_cond);
       }
       void WakeupConsumer()
       {
          std::cout <<"wake up consumer ..." << std::endl;
          pthread_cond_signal(&c_cond);
       }
       bool IsFull()
       {
          return _q.size() >= _cap;
       }
       bool IsEmpty()
       {
          return _q.empty();
       }
public:
       BlockQueue(size_t cap = 5)
         :_cap(cap)
       {
           pthread_mutex_init(&lock, nullptr);
           pthread_cond_init(&p_cond, nullptr);
           pthread_cond_init(&c_cond, nullptr);
       }
       ~BlockQueue()
       {
           pthread_mutex_destroy(&lock);
           pthread_cond_destroy(&p_cond);
           pthread_cond_destroy(&c_cond);
       }
       void Put(const T& t)
       {
           // 生产者
           LockQueue();
           while (IsFull())
           {
              WakeupConsumer();
              ProductorWait();
           }
           _q.push(t);
           UnlockQueue();
       }
       void Take(T& t)
       {
           // 消费者
           LockQueue();
           while (IsEmpty())
           {
              WakeupProductor();
              ConsumerWait();
           }
           t = _q.front();
           _q.pop();

           UnlockQueue();
       }
};

main.cc

#include "BlockQueue.hpp"
#include <unistd.h>
using namespace std;
pthread_mutex_t c_lock;
pthread_mutex_t p_lock;
void* consumer_run(void* arg)
{
    BlockQueue<Task>* pbq = (BlockQueue<Task>*)arg;
    
    while (true)
    {
        //int t = 0;
        pthread_mutex_lock(&c_lock);
        Task t;
        pbq->Take(t);
        sleep(1);
        //cout << "consume data :" << t << endl;
        cout<<"编号 "<< pthread_self()<<" 消费者" <<" consume task is "  <<  t._x <<  " + " << t._y << " = " << t.Run() << endl;
        pthread_mutex_unlock(&c_lock);
    }
}
void* productor_run(void* arg)
{
    sleep(1);
    BlockQueue<Task>* pbq = (BlockQueue<Task>*)arg;

    while (true)
    {
        pthread_mutex_lock(&p_lock);
        int x = rand()%10 + 1;
        int y = rand()%100 +1;
        Task t(x,y);
        pbq->Put(t);
       // cout << "product data :" << t << endl;
        cout <<"编号 " <<pthread_self()<< " 生产者" <<" product Task is : " << x  << " + " << y << " = ?" << endl;
        pthread_mutex_unlock(&p_lock);
        sleep(1);

    }
}
int main()
{
    BlockQueue<Task> bq;
    // 多消费者、多生产者
    pthread_t c1,c2,c3,p1,p2,p3;
    
    pthread_mutex_init(&c_lock, nullptr);
    pthread_mutex_init(&p_lock,nullptr);

    pthread_create(&c1, nullptr, consumer_run, (void*)&bq);
    pthread_create(&c2, nullptr, consumer_run, (void*)&bq);
    pthread_create(&c3, nullptr, consumer_run, (void*)&bq);
    pthread_create(&p1, nullptr, productor_run, (void*)&bq);
    pthread_create(&p2, nullptr, productor_run, (void*)&bq);
    pthread_create(&p3, nullptr, productor_run, (void*)&bq);

    pthread_join(c1,nullptr);
    pthread_join(c2,nullptr);
    pthread_join(c3,nullptr);
    pthread_join(p1,nullptr);
    pthread_join(p2,nullptr);
    pthread_join(p3,nullptr);

    pthread_mutex_destroy(&c_lock);
    pthread_mutex_destroy(&p_lock);
    return 0;
}

那么之前提到的问题:为什么 pthread_cond_wait需要互斥量(锁)===>在等待条件变量被其他线程通过访问临界资源打破等待的条件时,那么其他线程必须要有锁才可以,所以wait时候,必须释放锁,即该函数做了如下工作:自动释放lock ,当函数被返回的时候,返回到了临界区,则会让该线程重新持有锁。

  • 基于阻塞队列生产者消费者模型的应用场景:比如注册B站、抖音、都有这个模型的应用。

6.5 POSIX 信号量的引入

6.5.1 什么是信号量?

  • 信号量也称信号灯,本质上是一个描述临界资源有效个数的计数器

6.5.2 为什么使用信号量:使用信号量有什么好处?

  • POSIX 信号量 和 SystemV 信号量的作用相同,都是用于同步操作,达到无冲突访问共享资源的目的,但POSIX 可以用于线程间同步。可以把临界资源分成多份,多线程对每一份的临界资源进行同步访问,大大的提高效率。
struct sem {
   int count;
   mutex lock;
   wait_queue *head;
}
// P() 操作的伪代码如下
P(){
   lock();
   if (count > 0) count--;
   else 
      wait
   unlock();
}
V(){
   lock();
   if (count == 原值) wait;
   else
      count++;
   unlock();
}

6.5.3 如何使用信号量

1 初始化信号量

#include <semaphore.h> 
int sem_init(sem_t *sem, int pshared, unsigned int value); 
//参数:pshared:0表示线程间共享,非零表示进程间共享 value:信号量初始值

2 销毁信号量

int sem_destroy(sem_t *sem);

3 等待信号量

// 功能:等待信号量,会将信号量的值减1 
int sem_wait(sem_t *sem); //P()

4 发布信号量

//功能:发布信号量,表示资源使用完毕,可以归还资源了。将信号量值加1。 
int sem_post(sem_t *sem);//V()

6.6 基于环形队列的生产消费模型

  • 环形队列采用数组模拟,用模运算来模拟环状特性
  • 环形结构起始状态和结束状态都是一样的,不好判断为空或者为满,所以可以通过加计数器或者标记位来判断满或者空。另外也可以预一个空的位置,作为满的状态
  • 但是我们现在有信号量这个计数器,环形队列为空或者为满都可以由两个信号量来判断,具体的我会在代码中注释,就很简单的进行多线程间的同步过程

代码如下
Makefile

main:main.cc
	g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f main 

RingQueue.hpp

#pragma once 

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

template <class T>
class RingQueue
{
  private:
        std::vector<T> _v;
        int _cap;
        // 两个信号量
        sem_t c_sem_data;
        sem_t p_sem_blank;
        // 两个下标索引
        
        int c_index;
        int p_index;
  private:
        void P(sem_t &s)
        {
          sem_wait(&s);
        }
        void V(sem_t &s)
        {
          sem_post(&s);
        }

  public:
        RingQueue(int cap)
          :_cap(cap),_v(cap)
        {
           sem_init(&c_sem_data, 0, 0);
           sem_init(&p_sem_blank, 0, cap);
           c_index = p_index = 0;
        }
        
        ~RingQueue()
        {
           sem_destroy(&c_sem_data);
           sem_destroy(&p_sem_blank);
           c_index = p_index = 0;
        }
        void Put(T &in)
        {
            P(p_sem_blank);
            _v[p_index] = in;
            p_index++;
            p_index %= _cap;
            V(c_sem_data);
        }
        void Get(T &out)
        {
          // out 为输出型参数,由调用者传入引用获取内容
          P(c_sem_data);
          out = _v[c_index];
          c_index++;
          c_index %= _cap;
          V(p_sem_blank);
        }
};

main.cc

#include "RingQueue.hpp"
#include <unistd.h>

using namespace std;

void *consumer(void *arg)
{
  RingQueue<int>* rq = (RingQueue<int>*)arg;

  while (true)
  {
      sleep(1);
      int t;
      rq->Get(t);
      cout << "consumer done ..." << t << endl;
  }

}
void *productor(void* arg)
{
  RingQueue<int>* rq = (RingQueue<int>*)arg;
  int count = 100;
  while (true)
  {
    rq->Put(count);
    count++;
    if (count > 110)
    {
        count = 100;
    }
    cout << "productor done" << endl;
  }
}
int main()
{
    pthread_t c,p;
    RingQueue<int> rq(5);
    
    pthread_create(&c, nullptr, consumer, &rq);
    pthread_create(&p, nullptr, productor,&rq);

    pthread_join(c,nullptr);
    pthread_join(p,nullptr);
    return 0;
}

7 线程池

7.1 什么是线程池

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

7.2 线程池的应用场景

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

7.3 线程池的好处

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

7.4 模拟实现线程池

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

我们让主线程充当从网络中接受客户端请求的角色,每个请求是一个任务被送进任务队列,等待线程池里的线程来处理任务,我们的任务暂时简单的描述为:求一个数字的平方,等待后面更新计算机网络的时候可以完成一个小型的项目。

Makefile

testThreadPool:main.cc
	g++ $^ -o $@ -std=c++11 -lpthread
.PHONY:clean
clean:
	rm -f testThreadPool

ThreadPool.hpp

#pragma once

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

#define NUM 5

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; //only consumer, thread pool thread;
        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 ThreadWakeup()
        {
            //if(low_water > 30){
            //    pthread_cond_broadcast(&cond);
            //}
            pthread_cond_signal(&cond);
        }
        void ThreadsWakeup()
        {
            pthread_cond_broadcast(&cond);
        }
    public:
	    // 构造函数里尽量不要做有风险的事情
        ThreadPool(int _max=NUM):max_num(_max),quit(false)
        {}
        static void* Routine(void *arg) //
        {
			//线程分离
			pthread_detach(pthread_self());
            ThreadPool *this_p = (ThreadPool*)arg;
            while(!quit){
                this_p->LockQueue();
                while(!quit && this_p->IsEmpty()){
                    this_p->ThreadWait();
                }

                Task t;
                if(!quit && !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);
            }
        }
        //server
        void Put(Task &in)
        {
            LockQueue();
            q.push(&in);
            UnlockQueue();

            ThreadWakeup();
        }
        //Thread pool t;
        void Get(Task &out)
        {
            Task*t = q.front();
            q.pop();
            out = *t;
        }
        void ThreadQuit()
        {
            if(!IsEmpty()){
                std::cout << "task queue is not empty" << std::endl;
                return;
            }
            quit = true;
            ThreadsWakeup();
        }
        ~ThreadPool()
        {
            pthread_mutex_destroy(&lock);
            pthread_cond_destroy(&cond);
        }

};

main.cc

#include "ThreadPool.hpp"

int main()
{
    ThreadPool *tp = new ThreadPool();
    tp->ThreadPoolInit();

    //server
    int count = 20;
    while(count){
        int x = rand()%10+1;
        Task t(x);
        tp->Put(t);
        sleep(1);
        count--;
    }

    tp->ThreadQuit(); //
    return 0;
}

7.5 线程池VS进程池

  • 线程池占用的资源更少,但是健壮性(鲁棒性)不强。
  • 进程池占用的资源更多,但是健壮性(鲁棒性)很强。
  • 7
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值