首页 > 其他分享 >C 语言版线程池

C 语言版线程池

时间:2023-04-16 22:46:14浏览次数:48  
标签:int void 队列 任务 线程 shutdown 语言版

一、初始线程池

1.1 何为线程池?

我们先来打个比方,线程池就好像一个工具箱,我们每次需要拧螺丝的时候都要从工具箱里面取出一个螺丝刀来。有时候需要取出一个来拧,有时候螺丝多的时候需要多个人取出多个来拧,拧完自己的螺丝那么就会把螺丝刀再放回去,然后别人下次用的时候再取出来用。

说白了线程池就是相当于「提前申请了一些资源,也就是线程」,需要的时候就从线程池中取出线程来处理一些事情,处理完毕之后再把线程放回去。

线程池.drawio

1.2 为什么要使用线程池?

我们来思考一个问题,为什么需要线程池呢?假如没有线程池的话我们每次调用线程是什么样子的?

显然首先是先创建一个线程,然后把任务交给这个线程,最后把这个线程销毁掉。这样实现起来非常简便,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程是需要消耗时间的。

那么如果我们改用线程池的话,在程序运行的时候就会首先创建一批线程,然后交给线程池来管理。有需要的时候我们从线程池中取出线程用于处理任务,用完后我们再将其回收到线程池中,这样是不是就避免了每次都需要创建和销毁线程这种耗时的操作。

有人会说你使用线程池一开始就消耗了一些内存,之后一直不释放这些内存,这样岂不是有点浪费。其实这是类似于空间换时间的概念,我们确实多占用了一点内存但是这些内存和我们珍惜出来的这些时间相比,是非常划算的。

池的概念是一种非常常见的空间换时间的概念,除了有线程池之外还有进程池、内存池等等。其实他们的思想都是一样的就是我先申请一批资源出来,然后就随用随拿,不用再放回来。

1.3 如何设计线程池

线程池的组成主要分为 3 个部分,这三部分配合工作就可以得到一个完整的线程池:

  1. 任务队列:存储需要处理的任务,由工作的线程来处理这些任务。
    • 通过线程池提供的 API 函数,将一个待处理的任务添加到任务队列,或者从任务队列中删除
    • 已处理的任务会被从任务队列中删除
    • 线程池的使用者,也就是调用线程池函数往任务队列中添加任务的线程就是生产者线程
  2. 工作的线程(任务队列任务的消费者):若干个,一般情况下根据 CPU 的核数来确定。
    • 线程池中维护了一定数量的工作线程,他们的作用是不停的读任务队列,从里边取出任务并处理
    • 工作的线程相当于是任务队列的消费者角色
    • 如果任务队列为空,工作的线程将会被阻塞 (使用条件变量 / 信号量阻塞)
    • 如果阻塞之后有了新的任务,由生产者将阻塞解除,工作线程开始工作
  3. 管理者线程(不处理任务队列中的任务):1 个
    • 它的任务是周期性的对任务队列中的任务数量以及处于忙状态的工作线程个数进行检测
    • 当任务过多的时候,可以适当的创建一些新的工作线程
    • 当任务过少的时候,可以适当的销毁一些工作的线程

二、C 语言版线程池

由于本篇是对线程池的简单介绍,所以简化了一下线程池的模型,将 1.3 中的「3. 管理者线程」的角色给去除了。

2.1 结构体定义

2.1.1 任务结构体

/* 任务结构体 */
typedef struct
{
    void (*function)(void *);
    void *arg;
} threadpool_task_t;

2.1.2 线程池结构体

/* 线程池结构体 */
typedef struct
{
    pthread_mutex_t lock;       // 线程池锁,锁整个的线程池
    pthread_cond_t notify;      // 条件变量,用于告知线程池中的线程来任务了

    int thread_count;           // 线程池中的工作线程总数
    pthread_t *threads;         // 线程池中的工作线程
    int started;                // 线程池中正在工作的线程个数

    threadpool_task_t *queue;   // 任务队列
    int queue_size;             // 任务队列能容纳的最大任务数
    int head;                   // 队头 -> 取任务
    int tail;                   // 队尾 -> 放任务
    int count;                  // 任务队列中剩余的任务个数

    int shutdown;               // 线程池状态, 0 表示线程池可用,其余值表示关闭
} threadpool_t;
  • thread_count 和 started 的区别:

    • 初始化线程池的时候会创建一批线程(假设创建 n 个),此时 thread_count = started = n
    • 当线程池运行过程中可能需要关闭一些线程(假设关闭 m 个),则会销毁这些线程,并 started -= n,但 thread_count 保持不变
    • 即 thread_count 表示线程池中的申请的线程个数,而 started 表示当前能用的线程个数
  • shutdown 的作用:如果需要销毁线程池,那么必须要现将所有的线程退出才可销毁,而 shutdown 就是用于告知正在工作中的线程,线程池是否关闭用的。关闭方式又分为两种:一种是立即关闭,即不管任务队列中是否还有任务;另一种是优雅的关闭,即先处理完任务队列中的任务后再关闭。这两种方式可通过设置 shutdown 的不同取值即可实现:

    typedef enum
    {
        immediate_shutdown  = 1,    // 立即关闭线程池
        graceful_shutdown   = 2     // 等线程池中的任务全部处理完成后,再关闭线程池
    } threadpool_shutdown_t;
    

2.2 函数定义

2.2.1 ThreadPool_Init

函数原型:int ThreadPool_Init(int thread_count, int queue_size, threadpool_t **ppstThreadPool);

头 文 件:#include "ThrdPool.h"

函数功能:初始化线程池

参数描述:

  1. thread_count:入参,代表此次创建的线程池中的线程个数
  2. queue_size:入参,代表任务队列大小
  3. ppstThreadPool:出参,如果创建成功,则代表创建好的线程池,否则为 NULL

返 回 值:成功返回 E_SUCCEED,失败返回 E_ERROR

2.2.2 ThreadPool_Dispatch

函数原型:int ThreadPool_Dispatch(threadpool_t *pstThreadPool, void (*function)(void *), void *arg);

头 文 件:#include "ThrdPool.h"

函数功能:向线程池的任务队列中分发任务

参数描述:

  1. pstThreadPool:入参,代表创建好的线程池
  2. function:入参,表示任务
  3. arg:入参,代表 function 的参数

返 回 值:成功返回 E_SUCCEED,失败返回 E_ERROR

2.2.2 Threadpool_Destroy

函数原型:void Threadpool_Destroy(threadpool_t *pool, threadpool_shutdown_t shutdown_mode);

头 文 件:#include "ThrdPool.h"

函数功能:销毁线程池

参数描述:

  1. pool:入参,表示需要销毁的线程池
  2. shutdown_mode:入参,表示关闭模式,有两种取值

2.3 源码

2.3.1 ThrdPool.h

#ifndef __THRDPOOL_H__
#define __THRDPOOL_H__

#include <pthread.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>

#define DEBUG(format, args...) \
            printf("[%s:%d] "format"\n", \
                                __FILE__, \
                                __LINE__, \
                                ##args)

#define MAX_THREADS 16      // 线程池最大工作线程个数
#define MAX_QUEUE   256     // 线程池工作队列上限

#define E_SUCCEED   0
#define E_ERROR     112

#define SAFE_FREE(ptr) \
            if (ptr) \
            { \
                free(ptr); \
                ptr = NULL; \
            }

/* 任务结构体 */
typedef struct
{
    void (*function)(void *);
    void *arg;
} threadpool_task_t;

/* 线程池结构体 */
typedef struct
{
    pthread_mutex_t lock;       // 线程池锁,锁整个的线程池
    pthread_cond_t notify;      // 条件变量,用于告知线程池中的线程来任务了

    int thread_count;           // 线程池中的工作线程总数
    pthread_t *threads;         // 线程池中的工作线程
    int started;                // 线程池中正在工作的线程个数

    threadpool_task_t *queue;   // 任务队列
    int queue_size;             // 任务队列能容纳的最大任务数
    int head;                   // 队头 -> 取任务
    int tail;                   // 队尾 -> 放任务
    int count;                  // 任务队列中剩余的任务个数

    int shutdown;               // 线程池状态, 0 表示线程池可用,其余值表示关闭
} threadpool_t;

typedef enum
{
    immediate_shutdown  = 1,    // 立即关闭线程池
    graceful_shutdown   = 2     // 等线程池中的任务全部处理完成后,再关闭线程池
} threadpool_shutdown_t;

int ThreadPool_Init(int thread_count, int queue_size, threadpool_t **ppstThreadPool);
int ThreadPool_Dispatch(threadpool_t *pstThreadPool, void (*function)(void *), void *arg);
void Threadpool_Destroy(threadpool_t *pool, threadpool_shutdown_t shutdown_mode);

#endif

2.3.2 ThrdPool.c

#include <stdio.h>
#include "ThrdPool.h"

#define THREAD_COUNT    4
#define QUEUE_SIZE      128

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化锁,用于保证第16行的完整输出

void func(void *arg)
{
    static int num = 0;

    pthread_mutex_lock(&mutex);

    // 为方便观察,故特意输出该语句,并使用num来区分不同的任务
    DEBUG("这是执行的第 %d 个任务", ++num); 

    usleep(100000); // 模拟任务耗时,100ms

    pthread_mutex_unlock(&mutex);

    return;
}

int main()
{
    int iRet;
    threadpool_t *pool;
    iRet = ThreadPool_Init(THREAD_COUNT, QUEUE_SIZE, &pool);
    if (iRet != E_SUCCEED)
    {
        return 0;
    }

    int i;
    for (i = 0; i < 20; i++)    // 生产者,向任务队列中塞入 20 个任务
    {
        ThreadPool_Dispatch(pool, func, NULL);
    }

    usleep(500000);

    // Threadpool_Destroy(pool, immediate_shutdown);   // 立刻关闭线程池
    Threadpool_Destroy(pool, graceful_shutdown); // 等任务队列中的任务全部执行完毕再关闭

    return 0;
}

2.3.3 main.c

#include <stdio.h>
#include "ThrdPool.h"

#define THREAD_COUNT    4
#define QUEUE_SIZE      128

pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化锁,用于保证第 15 行的完整输出

void func(void *arg)
{
    static int num = 0;

    pthread_mutex_lock(&mutex);

    DEBUG("这是执行的第 %d 个任务", ++num); // 为方便观察,故特意输出该语句,并使用num来区分不同的任务

    usleep(100000); // 模拟任务耗时,100ms

    pthread_mutex_unlock(&mutex);

    return;
}

int main()
{
    int iRet;
    threadpool_t *pool;
    iRet = ThreadPool_Init(THREAD_COUNT, QUEUE_SIZE, &pool);
    if (iRet != E_SUCCEED)
    {
        return 0;
    }

    int i;
    for (i = 0; i < 20; i++)    // 生产者,向任务队列中塞入 20 个任务
    {
        ThreadPool_Dispatch(pool, func, NULL);
    }

    usleep(500000);

    // Threadpool_Destroy(pool, immediate_shutdown);   // 立刻关闭线程池
    Threadpool_Destroy(pool, graceful_shutdown); // 等任务执行完毕后方可关闭

    return 0;
}

2.4 Tutorial

2.4.1 目录结构

image-20230416223321675

2.4.2 编译、运行

image-20230416223355220

参考资料

标签:int,void,队列,任务,线程,shutdown,语言版
From: https://www.cnblogs.com/hyacinthLJP/p/17324315.html

相关文章

  • 线程
    1.轻量级锁(自旋锁)无等待队列,忙等待。场景:线程执行时间短,等待线程少。2.重量级锁有等待队列,通过操作系统调度。场景:线程执行时间长,等待线程多。  关键字valatile:......
  • C++实现多线程
    #include<iostream>#include<chrono>#include<thread>voidprintNumbers1(){for(inti=1;i<=10000;i++){std::cout<<"Thread1:"<<i<<std::endl;}}voidprintNumbers2(){for......
  • Java中创建线程的方式以及线程池创建的方式、推荐使用ThreadPoolExecutor以及示例
    场景Java中创建线程的方式有三种1、通过继承Thread类来创建线程定义一个线程类使其继承Thread类,并重写其中的run方法,run方法内部就是线程要完成的任务,因此run方法也被称为执行体,使用start方法来启动线程。2、通过实现Runanle接口来创建线程首先定义Runnable接口,并重写Runnable接口......
  • 第5章 高效的多线程日志
    日志库介绍:一个日志库大体可分为前端(frontend)和后端(backend)两部分。前端是供应用程序使用的接口(API),并生成日志消息(logmessage);后端则负责把日志消息写到目的地(destination)。在多线程程序中,前端和后端都与单线程程序无甚区别,无非是每个线程有自己的前端,整个程序共用一个后端。但难点......
  • 系统iowait和线程的iowait
     系统iowait和线程的iowait系统iowait时间统计在系统时钟中断时,会调用account_process_tick,如果是usertick,则增加usertime;否则如果current线程不是idle,则增加systemtime;否则增加idle时间,idle可以再分为iowait和纯粹的idle,如果当前cpu的runqueue的nr_iowait大于0,则增加iowai......
  • 多线程
    一.多线程1.什么是线程要了解线程,首先需要知道进程。一个进程指的是一个正在执行的应用程序。线程对应的英文名称为“thread”,它的功能是执行应用程序中的某个具体任务,比如一段程序、一个函数等。线程和进程之间的关系,类似于工厂和工人之间的关系,进程好比是工厂,线程就如同工厂......
  • python3多线程-线程池和优先队列
    1、介绍有两种线程池方案。各线程持续存在,从任务池获取任务进行执行按照需求创建线程,每个线程只执行一个任务,结束完毕则该线程结束2、准备(1)任务池task_list任务池是用于准备各任务单元的环境,比如http爆破时的请求参数,读写文件时的路径。任务池的准备可能会占用一定时间,边准......
  • python3多线程-线程同步
    1、介绍多线程同时访问和操作同一资源,可能会造成异常。解决办法是对资源进行限制,同一时间只允许一个线程进行访问和操作。这里的资源,一般是指方法、函数或者代码块。即由多行代码组成,在逻辑上属于不可切分的操作。2、线程同步使用Thread对象的Lock和Rlock可以实现简单......
  • java -- 线程(二)
    死锁死锁是指两个或两个以上的线程在执行过程中,由于竞争同步锁而产生的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的线程称为死锁。死锁的案例:同步代码块的嵌套创建锁对象:publicclassLock{public......
  • [计科]多进程和多线程的程序在使用上有何区别?
    区别多进程和多线程的程序在使用方式和效果上有很大的区别。多进程程序是在不同的进程之间进行协作的,每个进程都有自己独立的内存空间和系统资源。多个进程之间通过进程间通信(IPC)进行数据和信息的交换。多进程的优点在于可靠性和鲁棒性较高,一个进程出现问题不会影响到其它进程......