Unix network programming (4) thread pool concurrent server

concept

The thread pool is an abstract concept. It can be simply considered that several threads run together, and the threads do not exit, waiting for tasks to be processed.
Why is there a thread pool?

  • Take the server side of network programming as an example. As the server side supports high concurrency, multiple clients can connect and send requests. For multiple requests, we create threads every time, so that many threads will be created, and threads will be destroyed after execution. There will be a lot of system overhead, and the efficiency of use is very low.
  • It is not always better to create more threads, so our idea is to create several threads in advance, not to exit, to wait for the task to be generated, to receive the task and wait for the next task after processing.

insert image description here

Thread pool and task queue

How is the thread pool implemented? Need to think about 2 questions?

1. Assuming that the thread pool is created, how do the threads coordinate to receive tasks and process them?
2. How can the threads on the thread pool perform different request tasks?

The above problem 1 is very similar to the producer and consumer model. The client corresponds to the producer, and the thread pool on the server side corresponds to the consumer. It needs to be solved with the help of mutexes and condition variables.
The solution to problem 2 is to use the callback mechanism. We can also use the structure to encapsulate the task. For example, the data of the task and the task processing callback are encapsulated in the structure. In this way, when the worker thread of the thread pool gets the task, Also know how to do it.

task queue

A task contains two elements: the callback function and its parameters, and various attribute data of the task

The number is used here to identify the attributes of the task, so the structure of the task is defined as follows:

typedef _Task
{
	unsigned int tasknum;			//任务编号
	void (*task_func)(void *arg);		//回调函数
	void *arg;						//回调函数的参数
}Task;

Thread Pool

The core parts of the thread pool include:

  • task array (queue)
  • array of threads

The supporting auxiliary parts are:

  • Add mutex and condition variable for exclusive access
  • Attributes of the task array: the maximum number of tasks, the current number of tasks, the subscripts of tasks leaving and entering the team
  • Attributes of the thread array: the number of threads in the thread pool, whether the current thread is enabled (mark whether to destroy the thread)
typedef _ThreadPoll
{
	Task *tasks;						//任务队列
	unsigned int max_job_num;			//最大任务个数
	unsigned int job_num;				//实际任务个数
	unsigned int job_push;				//入队位置
	unsigned int job_pop;				//出队位置
	
	pthread_t *threads;					//线程池内线程数组
	unsigned int thr_num;				//线程池内线程个数
	int shutdown;						//是否关闭线程池

	pthread_mutex_t pool_lock;			//线程池的锁
	pthread_cond_t empty_task;			//任务队列为空的条件变量
	pthread_cond_t not_empty_task;		//任务队列不为空的条件变量
}ThreadPool;

Functions that operate the thread pool

Initialize the thread pool

void create_threadpool(int thrnum,int maxtasknum);
//Create a thread pool – thrnum represents the number of threads, maxtasknum is the maximum number of tasks

Steps:
Allocate array space for task queue and thread queue
Create threads in a loop
Initialize other attribute variables of the thread pool

//创建线程池并初始化
void create_threadpool(int thrnum, int maxtasknum, ThreadPool *thrPool)
{
	printf("initializing the ThreadPool\n");
	//分配线程数组和任务数组
	thrPool->threads = (pthread_t*)malloc(sizeof(pthread_t)*thrnum);
	thrPool->tasks = (Task*)malloc(sizeof(Task)*maxtasknum);

	//初始化ThreadPool 的属性
	thrPool->max_job_num = maxtasknum;
	thrPool->job_num = 0;
	thrPool->job_pop = 0;
	thrPool->job_push = 0;

	thrPool->thr_num = thrnum;
	thrPool->shutdown = 0;				//是否销毁线程池,1代表销毁
	
	pthread_mutex_init(&thrPool->pool_lock, NULL);
	pthread_cond_init(&thrPool->empty_task, NULL);
	pthread_cond_init(&thrPool->not_empty_task, NULL);

	//创建线程, 设置默认为detach方式运行
	pthread_attr_t attr;
	pthread_attr_init(&attr);
	pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);
	for(int i=0; i<thrnum; i++)
	{
		pthread_create(&thrPool->threads[i], &attr, (void*)thrRun, (void*)thrPool);
	}
	printf("ThreadPool created\n");
}

When creating a thread, the entry function thrRun needs to be given. The core function of this function is to fetch tasks from the task queue for execution, and then fetch tasks after execution, and repeat the above steps.
Notice:

  • Add a mutex lock before taking a task from the task queue, delete the task from the task queue immediately after taking it out, and release the lock
  • Copy the task from the task queue to the thread space and release it when the thread is destroyed
  • The thread loop executes until shutdown is set to 1, that is, the life cycle of the thread lasts until the thread pool is destroyed
void thrRun(void *arg)
{
	ThreadPool *thrPool = (ThreadPool*)arg;
	unsigned int taskpose = 0;
	Task *task = (Task*)malloc(sizeof(Task));
	//将任务取出,并立即从任务队列删除,并让出互斥锁
	
	while(1)
	{
		//加锁,获取任务
		pthread_mutex_lock(&thrPool->pool_lock);
		
		//任务队列为空并且不是要摧毁线程池,线程阻塞,等待任务到来
		while(thrPool->job_num <=0 && !thrPool->shutdown)
		{
			//阻塞成功就会释放pool_lock, 被唤醒后再加锁
			pthread_cond_wait(&thrPool->not_empty_task, &thrPool->pool_lock);
		}
		if(thrPool->job_num)
		{
			//取任务,修改线程池的任务队列和属性
			taskpose = (thrPool->job_pop++) % thrPool->max_job_num;
			memcpy(task, &thrPool->tasks[taskpose], sizeof(Task));
			task->arg = task;
			thrPool->job_num--;
			thrPool->job_pop = thrPool->job_pop % thrPool->max_job_num;
			//任务队列有空位置,通知任务生产者
			pthread_cond_signal(&thrPool->empty_task);
		}

		//线程池处于销毁状态
		if(thrPool->shutdown)
		{
			//退出线程, 并销毁线程使用的task的空间
			pthread_mutex_unlock(&thrPool->pool_lock);
			free(task);
			pthread_exit(NULL);
		}

		//任务获取成功,释放锁,执行回调函数
		pthread_mutex_unlock(&thrPool->pool_lock);
		task->task_func(task->arg);
	}
}

Destroy the thread pool

Core steps: set shutdown, release memory
Note:

  • A blocking queue that is waiting for a task needs to be woken up first (booby trap)
  • The resource requested by the child thread will be released when shutdown is 1
  • The main thread needs to wait for all the sub-threads to end (set joinable)
// 销毁线程池
void destroy_threadpool(ThreadPool *pool)
{
	//销毁线程,激活线程的销毁
	pool->shutdown = 1;
	//唤醒所有阻塞在等待任务步骤的线程,通知其自毁
	pthread_cond_broadcast(&pool->not_empty_task);
	
	//设置joinable,等待所有子线程退出
	for(int i=0; i<pool->thr_num; i++)
	{
		pthread_join(pool->threads[i], NULL);
	}

	//释放内存
	pthread_cond_destroy(&pool->not_empty_task);
	pthread_cond_destroy(&pool->empty_task);
	pthread_mutex_destroy(&pool->pool_lock);

	free(pool->tasks);
	free(pool->threads);
}

Add tasks to the thread pool

Core function: add tasks to the task queue, and notify the thread blocked because the task queue is empty
Note:

  • When the queue is not empty, block the signal waiting for the queue to have an empty position (empty_task)
//向线程池添加任务
void addtask(ThreadPool *pool, int *beginnum)
{
	//加锁
	pthread_mutex_lock(&pool->pool_lock);
	
	//等待任务队列有空位置
	while(pool->max_job_num <= pool->job_num)
	{
		pthread_cond_wait(&pool->empty_task, &pool->pool_lock);
	}

	//修改线程池属性
	int taskpose = (pool->job_push++)%pool->max_job_num;
	pool->job_push = pool->job_push % pool->max_job_num;
	(*beginnum) = (*beginnum) % 100000;
	pool->tasks[taskpose].tasknum = (*beginnum)++;
	pool->job_num++;

	pool->tasks[taskpose].arg = (void*)&pool->tasks[taskpose];
	pool->tasks[taskpose].task_func = taskRun;

	//释放锁,并通知等待not_empty信号的线程
	pthread_mutex_unlock(&pool->pool_lock);
	pthread_cond_signal(&pool->not_empty_task);
}

Task callback function

For simplicity, the callback function here prints the thread number and task number, and then sleeps for 2 seconds

//任务回调函数
void taskRun(void *arg)
{
	Task *task = (Task*)arg;
	int num = task->tasknum;
	printf("task %d is running %lu\n", num, pthread_self());

	sleep(1);
	printf("task %d is done %lu\n", num, pthread_self());
}

test

Test 4 thread task queue for 40 programs

int main()
{
	ThreadPool thrPool;
	create_threadpool(4, 40, &thrPool);
	int beginnum = 0;
	for(int i=0; i<80; i++)
		addtask(&thrPool, &beginnum);
	
	sleep(30);
	destroy_threadpool(&thrPool);

	return 0;
}

partial screenshot
insert image description here
full code

Guess you like

Origin blog.csdn.net/qq_55796594/article/details/128275119