手撕线程池与性能测试

线程池相关介绍

线程池是什么?

维持和管理固定数量线程的池式结构。

为什么要维持固定数量线程?

线程数量的继续增加,由于系统资源的限制,不再带来性能的提升,反而带来负担。同时也是为了避免频繁地创建和销毁线程。

线程池如何管理线程?

有任务的时候让线程执行,没有任务的时候让线程休眠。

线程池的组成部分?

广义的线程池:固定数量的线程 + 任务队列,以及配套的用于管理的结构体与接口(C语言中)。

狭义的线程池:固定数量的线程 + 任务队列。

任务存储为什么要用队列数据结构?

主要是方便按先到到达的次序执行任务。

固定多少数量的线程?

根据经验,一般这么配置:

  1. cpu密集型。Cpu核心数。
  2. io密集型。2倍cpu核心数+2。原因可理解为IO操作从用户态陷入到内核态,再从内核态上升到用户态,需要2次上下文的切换,切换期间占用的时间比较长,这期间cpu可以由其他线程接手。

线程池如何工作?

利用生产消费模型。单线程下,一个线程既需要生成任务又需要执行任务,有了线程池之后,原线程成为生产者线程,只需要生产任务,然后把任务交给线程池里的线程执行,也就是消费者线程。

具体模型:生产者线程(不一定是一个)抛出任务;任务进入任务队列中,线程池(管理和维持消费者线程)接收任务执行;如果任务队列没有任务,消费者线程进入休眠状态。

什么时候要使用线程池?

某些任务特别耗时,严重阻塞该线程(生产者线程)执行其他任务。

用C语言手撕线程池

头文件设计

在这里,我们把接口设计相关的内容放在头文件中。用户一般只看头文件。

首先规范任务执行的上下文。听起来很复杂,其实上下文广义来说就是【事物运行依赖的资源】,比如线程上下文,包括线程运行的时候依赖的寄存器的值,线程堆栈的指针等等;函数上下文,包括函数参数、函数调用栈等;其他类似的上下文还有进程上下文、会话上下文等。规范任务执行的上下文,其实是说之后的代码里,任务结构体需要存储函数、函数的参数,那么在此规范函数形式和参数类型。

其次让用户知道有线程池存在,于是提供了一个标识。注意的是线程池可以不止一个。

最后设计一下接口。用户只需要知道调用线程池,可以通过哪些接口即可,不需要接口的具体实现。因此需要的接口包括创建线程池、暂停线程池、抛任务入线程池、回收线程等4个接口。

手撕线程池与性能测试_第1张图片

.c文件实现

数据结构体设计

管理线程数组的结构体

这个结构体用来管理线程池。需要包含以下元素:任务队列地址、退出标记、线程数量、线程数组地址。退出标记是一个原子变量,具备原子性,是线程安全的。

手撕线程池与性能测试_第2张图片

任务结构体

需要包含这些元素:下一个任务的指针、任务函数、函数上下文。

手撕线程池与性能测试_第3张图片

管理任务队列结构体

这个结构体是用来管理任务队列机构的,需要包含以下元素:任务队列头指针、任务队列尾指针、阻塞标识、不同类型的锁。这里着重说明尾指针为什么用二级指针,一级指针指向一个任务结构体,进行取值的时候取出来的是这个结构体本身;二级指针指向一个任务结构体,进行取数的时候,取出来的是一个地址,也就是任务结构体的第一个元素,这会极其方便我们操作。

手撕线程池与性能测试_第4张图片

常见问题分析

任务生命周期管理在哪里?

任务生命周期(即任务的创建和销毁)再线程池的内部代码中,不暴露给用户使用。

怎么根据队列的状态管理消费者线程?

线程池里的线程从队列中取出任务的时候,如果取出为空,那么进入休眠状态。

怎么让线程休眠?

进入阻塞状态即可,在这里使用加锁的手段,根据条件加锁。

锁为什么在队列结构中?

线程池的线程是消费者线程,消费者如果队列为空需要阻塞休眠,线程池跟队列的状态相关 。

接口实现

自己内部使用的接口可以加static修饰,暴露给用户的接口可以不修饰。

资源的创建尽量使用回滚编程,业务逻辑尽量使用防御编程。

回滚编程和防御编程都是用于处理错误和异常的编程方式,但是回滚式编程更注重回滚机制来保证数据的一致性,比如函数里某一资源申请失败了导致函数无法运行,那么要逐步释放之前申请成功的资源再退出函数,一切数据都没有变化。回滚防御编程更灵活,可以在发生不同的错误类型和正确执行的情况下都执行不同的代码进行处理。

创建线程池

如果管理线程数组结构体申请资源失败直接退出,如果能持续申请到资源注意让退出标记为0。

手撕线程池与性能测试_第5张图片

其中,创建任务队列结构体的代码如下,创建的时候也会伴随着任务队列的创建,注意一开始任务队列的阻塞值设为1,表示阻塞。

手撕线程池与性能测试_第6张图片

如果一路资源申请顺利,那么就创建线程数组。

手撕线程池与性能测试_第7张图片

如果某个线程创建失败那么注意回收全部线程,并且将任务队列的阻塞标识设置为阻塞,同时销毁任务队列及管理结构体。

手撕线程池与性能测试_第8张图片

手撕线程池与性能测试_第9张图片

手撕线程池与性能测试_第10张图片

如果全部线程创建成功,看看往线程里注册的执行函数。可以看出所有的执行函数都在等待任务结构体实例传进来然后进行执行。get_task这一步会阻塞住等待任务,因为这个函数没有任务的时候不给返回值,条件变量锁住了函数的执行,除非生产者线程主动终止线程池会返回空值。

手撕线程池与性能测试_第11张图片

这些消费者线程通过该函数获取到真正的任务函数。首先生产者线程会pop task。如果没有任务,并且任务队列处于非阻塞状态,那么就阻塞等待有任务进入队列。为什么pop task 的时候要使用自旋锁?如果使用互斥锁在上锁的时候,很容易会被其他线程夺走资源,如果没有锁那么又无法保证task queue的线程安全问题。

手撕线程池与性能测试_第12张图片

手撕线程池与性能测试_第13张图片

往任务队列添加任务

添加任务的时候可以用cond通知到正在阻塞等待能pop任务的任务队列。

手撕线程池与性能测试_第14张图片

手撕线程池与性能测试_第15张图片

退出函数与终止函数

手撕线程池与性能测试_第16张图片

线程池测试

单生产者线程多消费者线程

手撕线程池与性能测试_第17张图片

首先,将 thrd_pool.c 文件编译成位置无关的目标文件(.o 文件),以便后续进行链接操作生成可执行文件或共享库。

其次将thrd_pool编译成动他i链接库。

最后将动态链接库与测试文件进行联合编译。

手撕线程池与性能测试_第18张图片

多生产者多消费者

手撕线程池与性能测试_第19张图片

需要相关文件请私聊~~~~

你可能感兴趣的:(c语言,linux,服务器,运维,c++)