Memory pool explanation and thread pool (Linux) implementation

1. Memory pool

1. What is a memory pool

A memory pool is a memory allocation method . Before actually using the memory, apply for allocation of a certain number of memory blocks of equal size for backup. When there is a new memory demand, a part of the memory block is allocated from the memory pool, and if the memory block is not enough, continue to apply for new memory. The advantages of using the memory pool are: the speed is much faster than malloc/free, because the number of system calls is reduced , especially the frequent application/release of memory blocks.

However, using memory pools also has disadvantages:

1. If there are too many pre-allocated memory blocks, a lot of space will be wasted;

2. If there are too few pre-allocated memory blocks, it will frequently apply to the operating system for new memory blocks, thereby reducing program efficiency.

2. Disadvantages of traditional memory usage

1. The use of smaller memory blocks during high concurrency results in frequent system calls , reducing the execution efficiency of the system

2. Frequent use increases the fragmentation of system memory and reduces memory usage efficiency

3. There is no garbage collection mechanism, which can easily cause memory leaks and lead to memory exhaustion

4. When the logic of memory allocation and release is far apart in the program, the stability of the program will be reduced

3. Solution

1. Using components

2. Use the memory pool

Can solve most of the problems of traditional memory application and allocation

1. The memory pool pre-allocates a large block of memory in advance and releases it uniformly, which greatly reduces the calls of functions such as malloc and free.

2. The memory pool allocates a moderately sized memory block for each request, avoiding fragmentation

3. After the end of the life cycle, the memory is released uniformly, completely avoiding the occurrence of memory leaks

4. After the end of the life cycle, the memory is released uniformly to avoid repeated release of pointers or release of null pointers, etc.

4. Memory pool design

1. Design considerations for memory pools

  • The design logic should be as simple as possible to avoid mutual influence between different requests and minimize the coupling between different modules

  • The life time of the memory pool should be as short as possible, with the same cycle as the request or connection, to reduce fragmentation and memory leaks

2. Thread pool

1. What is a thread pool

The thread pool is a thread management technology implemented by using the idea of ​​pooling technology , mainly to reuse threads, manage threads and tasks conveniently, and decouple the creation of threads from the execution of tasks . The advantages of the thread pool are: control the number of running threads, put tasks into the queue during processing, and then start these tasks after the threads are created. If the number of threads exceeds the maximum number, the excess threads wait in line and wait for other threads to finish executing , and then take the task out of the queue to execute.

2. The steps to implement the thread pool in C++ are as follows:

1. Define a task class.

2. Define a thread pool class.

3. Define a task queue in the thread pool class.

4. Define a mutex and a condition variable in the thread pool class.

5. Define a function in the thread pool class to add tasks to the task queue.

6. Define a function in the thread pool class to take out tasks from the task queue and execute them.

7. Create a thread pool object in the main function and add tasks to it.

3. Implementation principle:

1. Task queue, which stores tasks that need to be processed, and these tasks are processed by working threads

  • Add a pending task to the task queue or delete it from the task queue through the API function provided by the thread pool

  • Processed tasks are removed from the task queue

  • The user of the thread pool, that is, the thread that calls the thread pool function to add tasks to the task queue is the producer thread

2. Working threads (consumers of task queue tasks), N

  • A certain number of worker threads are maintained in the thread pool. Their function is to read the task queue continuously, take out tasks from it and process them.

  • The working thread is equivalent to the consumer role of the task queue

  • If the task queue is empty, the working thread will be blocked (using a condition variable/semaphore to block)

  • If there is a new task after blocking, the producer will unblock and the worker thread will start working

3. Manager thread (not processing tasks in the task queue), 1

  • Its task is to periodically detect the number of tasks in the task queue and the number of worker threads in a busy state

  • When there are too many tasks, some new worker threads can be created appropriately

  • When there are too few tasks, some working threads can be properly destroyed

4. Implement the functions and structures used by the thread pool

所使用的函数都包含在头文件 <pthread.h> 中

1.pthread_mutex_t结构体

结构体原型:

typedef union
{
  struct __pthread_mutex_s __data;
  char __size[__SIZEOF_PTHREAD_MUTEX_T];
  long int __align;
} pthread_mutex_t;
    
    这是一个结构体常量,用来创建互斥锁常量,但是创建出来的互斥锁还不可以使用,需要初始化

初始化互斥锁有两种方式:

1.静态方式:使用PTHREAD_MUTEX_INITIALIZER进行初始化

pthread_mutex_t mutex_lock=PTHREAD_MUTEX_INITIALIZER;
            在LinuxThreads实现中,pthread_mutex_t是一个结构,
                而PTHREAD_MUTEX_INITIALIZER则是一个结构常量的宏。

    静态初始化条件变量只能拥有默认的条件变量属性,不能设置其他条件变量属性

2.动态方式:使用函数pthread_mutex_init进行初始化

int pthread_mutex_init(pthread_mutex_t *mutex, 
                        const pthread_mutexattr_t *mutexattr) 

    作用:初始化互斥锁
    参数解释:
        参数mutex:是pthread_mutex_t创建出来互斥锁变量
        参数mutexattr:用于指定互斥锁属性如下,如果为NULL则使用缺省属性。
            1.PTHREAD_MUTEX_TIMED_NP,这是缺省值,也就是普通锁。当一个线程加锁以后,
                其余请求锁的线程将形成一个等待队列,并在解锁后按优先级获得锁。
                这种锁策略保证了资源分配的公平性。
            2.PTHREAD_MUTEX_RECURSIVE_NP,嵌套锁,允许同一个线程对同一个锁成功获得多次,
                并通过多次unlock解锁。如果是不同线程请求,则在加锁线程解锁时重新竞争。 
            3.PTHREAD_MUTEX_ERRORCHECK_NP,检错锁,如果同一个线程请求同一个锁,
                则返回EDEADLK,否则与PTHREAD_MUTEX_TIMED_NP类型动作相同。
                这样就保证当不允许多次加锁时不会出现最简单情况下的死锁。
            4.PTHREAD_MUTEX_ADAPTIVE_NP,适应锁,动作最简单的锁类型,仅等待解锁后重新竞争。
    返回值:函数执行成功会返回0,失败返回非0,
            函数成功执行后,互斥锁被初始化为未锁住态

2.pthread_cond_t结构体

结构体原型:

typedef union
{
  struct __pthread_cond_s __data;
  char __size[__SIZEOF_PTHREAD_COND_T];
  __extension__ long long int __align;
} pthread_cond_t;
    
    创建一个条件变量,条件变量要与互斥量一起使用,条件本身是由互斥量保护的。
        线程在改变条件状态之前必须首先锁住互斥量
        其他线程在获得互斥量之前不会察觉到这种改变,因为互斥量必须在锁定以后才能计算条件

条件变量的初始化与互斥锁的初始化一样,静态初始化需要使用PTHREAD_COND_INITIALIZER 宏

3.pthread_create函数

函数原型:


int  pthread_create(pthread_t *tidp, const  pthread_attr_t *attr,
    ( void *)(*start_rtn)( void *), void  *arg);

    作用:创建一个线程
    参数解释:
        参数tidp:为指向线程 标识符的 指针,创建成功的线程id存放到该参数
        参数attr:用来设置线程属性
        参数3:线程运行函数的起始地址
        参数arg:运行函数的参数
    返回值:若线程创建成功,则返回0。若线程创建失败,则返回出错编号

4.pthread_join函数

函数原型:

int pthread_join(pthread_t thread, void **retval)
    作用:阻塞等待线程退出,获取线程退出状态
    参数解释:
        thread:线程ID (注意:不是指针);
        retval:存储线程结束状态
    返回值:成功返回0;失败返回错误号,使用strerror函数获取

5.pthread_cond_signal函数

函数原型:

int pthread_cond_signal (pthread_cond_t *__cond)
    解析:但是pthread_cond_signal在多处理器上可能同时唤醒多个线程,
当你只能让一个线程处理某个任务时,其它被唤醒的线程就需要继续
wait,而且规范要求pthread_cond_signal至少唤醒一个pthread_cond_wait上的线程,
其实有些实现为了简单在单处理器上也会唤醒多个线程. 另外,某些应用,如线程池,
pthread_cond_broadcast唤醒全部线程,但我们通常只需要一部分线程去做执行任务,
所以其它的线程需要继续wait.所以强烈推荐对pthread_cond_wait()使用while循环来做条件判断.
    作用:是发送一个信号给另外一个正在处于阻塞等待状态的线程,使其脱离阻塞状态,
继续执行.如果没有线程处在阻塞等待状态,pthread_cond_signal也会成功返回
    参数:pthread_cond_t创建出来的值,为条件变量
    返回值:成功返回0,失败返回错误编号

    该函数需要和函数pthread_cond_wait函数一起使用

6.pthread_cond_destroy函数

函数原型:

int pthread_cond_destroy (pthread_cond_t *__cond)
    作用:销毁条件变量
    参数:使用pthread_cond_t创建的条件变量,需要使用地址
    返回值:成功返回0,失败返回错误编号

7.pthread_mutex_destroy函数

函数原型:

int pthread_mutex_destroy(pthread_mutex_t *mutex);
    作用:销毁互斥锁
    参数:pthread_mutex_t创建的变量,需要使用地址
    返回值:成功返回0,失败返回错误编号

8.pthread_mutex_lock函数

函数原型:

int pthread_mutex_lock(pthread_mutex_t *mutex);

    作用:会阻塞线程使用被该函数锁住的内容,在操作线程池的公共资源时,需要获取到该锁的使用权。
如果这个锁此时正在被其它线程占用,那么 pthread_mutex_lock() 调用会进入到这个锁的排队队列中,
并会进入阻塞状态, 直到拿到锁之后才会返回
    参数:pthread_mutex_t创建的变量,互斥锁
    返回值:若成功返回0,否则返回错误编号

9.pthread_mutex_unlock函数

函数原型:

int pthread_mutex_unlock(pthread_mutex_t *mutex);
    
    作用:会释放被pthread_mutex_lock锁住的代码段内容,使之可以被其他线程调用
    参数:pthread_mutex_t创建的变量,互斥锁
    返回值:若成功返回0,否则返回错误编号

10.pthread_cond_wait函数

函数原型:

int pthread_cond_wait(pthread_cond_t * cond, pthread_mutex_t * mutex); 

    解析:pthread_cond_wait函数是一个线程同步函数,用于等待条件变量的信号。当线程调用该函数时,
它会自动释放锁并进入等待状态,直到另一个线程发出信号并通知该线程条件变量已经满足。此时,
该线程会重新获得锁并继续执行。该函数通常与pthread_cond_signal或pthread_cond_broadcast
函数一起使用,以实现线程间的同步
    参数解释:
        参数cond:要等待的条件变量
        参数mutex:被唤醒线程要获取到的锁
    返回值:成功返回0,失败返回非0
    

11.pthread_self函数

函数原型:

pthread_t pthread_self(void)
    pthread_self函数是一个POSIX线程函数,用于获取当前线程的线程ID。它返回一个pthread_t类型的值,
这是一个唯一标识线程的值。
    返回值:返回调用线程的线程ID

5.线程池的代码实现

1.main函数代码


#include "threadpool.h"

void taskFunc(void* arg)
{
    int num = *(int*)arg;
    printf("thread %ld is working, number = %d\n", pthread_self(), num);
    sleep(1);
}

int main()
{
    //创建线程池
    ThreadPool* pool = threadPoolCreate(3, 10, 100);
    //添加任务
    for (int i = 0; i < 100; i++)
    {
        int* num = (int*)malloc(sizeof(int));
        *num = i + 100;
        threadPoolAdd(pool, taskFunc, num);
    }

    //睡眠
    sleep(30);

    //销毁线程池
    threadPoolDestroy(pool);

    return 0;
}

2.threadpool.h代码


#include<stdio.h>
#include<pthread.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>

#ifndef _THREADPOOL_H
#define _THREADPOOL_H

typedef struct ThreadPool ThreadPool;

//创建线程池并初始化
ThreadPool* threadPoolCreate(int min, int max, int queuesize);

//销毁线程池
int threadPoolDestroy(ThreadPool* pool);

//给线程池添加任务
void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg);

//获取线程池中工作的线程的个数
int threadPoolBusyNum(ThreadPool* pool);

//获取线程池中活着的线程的个数
int threadPoolAliveNum(ThreadPool* pool);


void* worker(void* arg);//工作函数
void* manager(void* arg);//管理者线程
void threadExit(ThreadPool* pool);//

#endif //_THREADPOOL_H

3.threadpool.c代码

#include "threadpool.h"
#include<pthread.h>

const int NUMBER = 2;

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

//线程池结构体
struct ThreadPool
{
    Task* taskQ; //任务队列
    int queueCapa; //任务容量
    int queueSize; //队列大小
    int queueFront; //队头->取数据
    int queueRear; //队尾->放数据

    pthread_t managerID;        // 管理者线程ID
    pthread_t* threadIDs;        // 工作的线程ID
    int minNum;                    // 最小线程数量
    int maxNum;                    //最大线程数量
    int busyNum;                //忙的线程的个数
    int liveNum;                //存活的线程的个数
    int exitNum;                //要销毁的线程个数

    pthread_mutex_t mutexPool;    //锁整个的线程池
    pthread_mutex_t mutexBusy;    //锁住busyNum的变量

    //条件变量
    pthread_cond_t notFull;        //判断队列是否满了
    pthread_cond_t notEmpth;    //判断队列是否空了
    
    int shutdown;                //判断是否销毁线程池,销毁为1,不销毁为0
};

//创建线程池并初始化
ThreadPool* threadPoolCreate(int min, int max, int queuesize)
{
    //创建一个堆区内容
    ThreadPool* pool = (ThreadPool*)malloc(sizeof(ThreadPool));
    do
    {
        if (pool == NULL)
        {
            printf("malloc pool error...\n");
            break;
        }
        //创建线程的最大数量
        pool->threadIDs = (pthread_t*)malloc(sizeof(pthread_t) * (unsigned long)max);
        if (pool->threadIDs == NULL)
        {
            printf("malloc threadIDs error\n");
            break;
        }
        memset(pool->threadIDs, 0, sizeof(pthread_t)*(unsigned long)max);
        //初始化一些数据
        pool->maxNum = max;
        pool->minNum = min;
        pool->liveNum = min; //和刚创建的线程数相等
        pool->exitNum = 0;
        pool->busyNum = 0;

        //初始化锁和条件变量
        if (pthread_mutex_init(&pool->mutexPool, NULL) != 0 ||
            pthread_mutex_init(&pool->mutexBusy, NULL) != 0 ||
            pthread_cond_init(&pool->notEmpth, NULL) != 0 ||
            pthread_cond_init(&pool->notFull, NULL) != 0)
        {
            printf("mutex or cond init error..\n");
            break;
        }

        //初始化任务队列
        pool->taskQ = (Task*)malloc(sizeof(Task) * queuesize);
        pool->queueCapa = queuesize;
        pool->queueSize = 0;
        pool->queueFront = 0;
        pool->queueRear = 0;

        //设置线程池的初始状态
        pool->shutdown = 0;

        //创建线程
        pthread_create(&pool->managerID, NULL, manager, pool);//管理者线程
        for (int i = 0; i < min; i++)
        {
            pthread_create(&pool->threadIDs[i], NULL, worker, pool);//执行任务的线程
        }

        return pool;
    } while (0);

    //释放资源
    if (pool->threadIDs) free(pool->threadIDs);
    if (pool->managerID) free(pool->managerID);
    if (pool->taskQ) free(pool->taskQ);
    if (pool) free(pool);

    return NULL;
}

int threadPoolDestroy(ThreadPool* pool)
{
    if (pool == NULL)
    {
        return -1;
    }
    //关闭线程池
    pool->shutdown = 1;
    //阻塞回收管理者线程
    pthread_join(pool->managerID, NULL);
    //唤醒阻塞的消费者线程
    for (int i = 0; i < pool->liveNum; i++)
    {
        pthread_cond_signal(&pool->notEmpth);
    }
    //释放堆内存
    if (pool->taskQ)
    {
        free(pool->taskQ);
    }
    if (pool->threadIDs)
    {
        free(pool->threadIDs);
    }
    //释放锁资源
    pthread_mutex_destroy(&pool->mutexPool);
    pthread_mutex_destroy(&pool->mutexBusy);
    pthread_cond_destroy(&pool->notEmpth);
    pthread_cond_destroy(&pool->notFull);

    //释放线程池
    free(pool);
    pool = NULL;

    return 0;
}

void threadPoolAdd(ThreadPool* pool, void(*func)(void*), void* arg)
{
    pthread_mutex_lock(&pool->mutexPool);
    while (pool->queueSize == pool->queueCapa && !pool->shutdown)//任务队列满了就阻塞
    {
        //阻塞生产者线程
        pthread_cond_wait(&pool->notFull, &pool->mutexPool);
    }
    if (pool->shutdown)//如果线程池关闭了
    {
        pthread_mutex_unlock(&pool->mutexPool);
        return;
    }
    //添加任务
    pool->taskQ[pool->queueRear].function = func;
    pool->taskQ[pool->queueRear].arg = arg;
    //移动队尾
    pool->queueRear = (pool->queueRear + 1) % pool->queueCapa;
    pool->queueSize++;//任务队列加1
    
    //需要唤醒阻塞在条件变量上工作线程区执行任务
    pthread_cond_signal(&pool->notEmpth);

    pthread_mutex_unlock(&pool->mutexPool);

}

//获取忙线程数量
int threadPoolBusyNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexBusy);
    int busyNum = pool->busyNum;
    pthread_mutex_unlock(&pool->mutexBusy);
    return busyNum;
}

//获取存活线程数量
int threadPoolAliveNum(ThreadPool* pool)
{
    pthread_mutex_lock(&pool->mutexPool);
    int liveNum = pool->liveNum;
    pthread_mutex_unlock(&pool->mutexPool);
    return liveNum;
}

//回调函数
void* worker(void* arg)
{


    struct ThreadPool* pool = (struct ThreadPool*)arg;
    while (1)
    {
        //使用线程池中的资源需要先加锁
        pthread_mutex_lock(&pool->mutexPool);//加锁
        //当任务队列为空,循环线程队列
        while (pool->queueSize == 0 && !pool->shutdown)
        {
            //阻塞工作线程
            pthread_cond_wait(&pool->notEmpth, &pool->mutexPool);//即可以阻塞线程

            //处理要销毁的线程
            if (pool->exitNum > 0)
            {
                pool->exitNum--;
                if (pool->liveNum > pool->minNum)
                {
                    pool->liveNum--;
                    pthread_mutex_unlock(&pool->mutexPool);//先解锁,不然会出现死锁
                    threadExit(pool);//让线程退出
                }
                
            }
        }
        //有线程被唤醒
        //判断线程池是否被锁住
        if (pool->shutdown)
        {
            pthread_mutex_unlock(&pool->mutexPool);//需要解锁
            threadExit(pool);//让当前线程退出
        }

        //从任务队列中取出一个任务
        Task task;//创建一个任务结构体
        task.function = pool->taskQ[pool->queueFront].function;//把线程池中的任务结构体中的第一个元素取出
        task.arg = pool->taskQ[pool->queueFront].arg;

        //移动头节点
        pool->queueFront = (pool->queueFront + 1) % pool->queueCapa;
        pool->queueSize--;//任务个数减1

        //取出一个任务后,需要唤醒生产者线程,进行添加任务
        pthread_cond_signal(&pool->notFull);

        pthread_mutex_unlock(&pool->mutexPool);//解锁

        printf("thread %ld start working...\n", pthread_self());
        //需要给忙线程队列加锁
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum++;//忙线程数量加1
        pthread_mutex_unlock(&pool->mutexBusy);

        //开始执行任务
        task.function(task.arg);//直接使用函数指针调用任务函数
        //(*task.function)(task.arg);//解引用,直接调用任务函数本身
        //释放一下内存
        free(task.arg);
        task.arg = NULL;

        printf("thread %ld end working...\n", pthread_self());
        //执行完任务后,要把忙线程改回去
        pthread_mutex_lock(&pool->mutexBusy);
        pool->busyNum--;//忙线程数量减1
        pthread_mutex_unlock(&pool->mutexBusy);
    }


    return NULL;
}

//管理者线程
void* manager(void* arg)
{
    struct ThreadPool* pool = (struct ThreadPool*)arg;
    while (!pool->shutdown)//当线程池没有被关闭
    {
        sleep(3);//每隔3秒检测一次
        //取出线程池中任务的数量和当前线程池的数量
        pthread_mutex_lock(&pool->mutexPool);//上锁
        int queueSize = pool->queueSize; //取出线程池中线程的数量
        int liveNum = pool->liveNum;    //取出线程存活的数量
        pthread_mutex_unlock(&pool->mutexPool);//解锁

        //取出忙线程的数量
        pthread_mutex_lock(&pool->mutexBusy);
        int busyNum = pool->busyNum;
        pthread_mutex_unlock(&pool->mutexBusy);

        //添加线程
        //添加原则:任务的个数>存活的线程个数  &&  存活的线程数 < 最大线程数  并且每次只加连个线程
        if (queueSize > liveNum && liveNum < pool->maxNum)
        {
            pthread_mutex_lock(&pool->mutexPool);//加锁
            int counter = 0;
            for (int i = 0; i < pool->maxNum && counter < NUMBER && pool->liveNum < pool->maxNum; i++)
            {
                if (pool->threadIDs[i] == 0)//只要线程数组中为空,就创建线程
                {
                    pthread_create(&pool->threadIDs[i], NULL, worker, pool);//创建线程
                    pool->liveNum++;//存活数加1
                    counter++;
                }
            }
            pthread_mutex_unlock(&pool->mutexPool);
        }

        //销毁线程
        //销毁原则:忙的线程数*2 < 存活的线程数 && 存活的线程 > 最小线程数,每次销毁还是销毁两个
        if (busyNum * 2 < liveNum && pool->minNum < liveNum)
        {
            pthread_mutex_lock(&pool->mutexPool);
            pool->exitNum = NUMBER;
            pthread_mutex_unlock(&pool->mutexPool);
            //让工作的线程自杀
            for (int i = 0; i < NUMBER; i++)
            {
                pthread_cond_signal(&pool->notEmpth);//唤醒线程
            }
        }

    }
    return NULL;
}

void threadExit(ThreadPool* pool)
{
    pthread_t tid = pthread_self();//获取线程id
    for (int i = 0; i < pool->maxNum; i++)
    {
        if (pool->threadIDs[i] == tid)
        {
            pool->threadIDs[i] = 0;
            printf("threadExit() called, %ld exiting...\n", tid);
            break;
        }
    }
    pthread_exit(NULL);
}

Guess you like

Origin blog.csdn.net/weixin_62859191/article/details/129700379