简易线程池的实现

简易线程池的实现

参考资料:

条件变量之虚假唤醒

互斥锁作用的理解

C++使用pthread实现的线程池PthreadPool

​ 线程池就是提前创建好一定数量的线程,使用条件变量阻塞这些线程,当有任务进来时,任意调用一个线程执行该任务。

  • 提前启动多个线程,当有任务来临时,调用线程执行任务,当没有任务时,线程阻塞。
  • 如果没有线程池,每当有任务来临时,需要创建线程,执行任务,任务执行完毕,销毁线程。如果有很多任务,则需要不断的创建线程,然后销毁线程,这会带来额外开销。
  • 使用线程池,可以省去创建线程、销毁线程导致的额外开销。

本文使用pthread来实现线程池

线程函数

​ pthread_create创建线程时,需要给线程指定一个执行函数,每个线程中都会执行这个函数,该函数为线程执行的主函数

线程主函数执行逻辑:

  • 线程中执行的函数会调用条件变量阻塞线程。
  • 当满足一定的条件(任务队列不为空),条件变量释放。线程由阻塞态转变为运行态
  • 线程取出任务队列中的数据机构,该数据机构是包含一个函数指针和函数执行参数的结构体。
  • 线程的主函数动态执行线程池任务队列中分配的任务函数

我觉得线程池中最妙的一点就是,使用包含函数指针和函数执行参数的结构体,把线程池中的线程需要执行的函数延后在运行期时动态绑定。

// 线程池中的每个线程都会执行该函数。
void* threadpool::thread_working_function(void* thread_pool) {
	// 获取线程池的实例
	threadpool* pool = (threadpool*)thread_pool;

	while (1) {
		// 线程获取互斥锁
		pthread_mutex_lock(&(pool->lock));

		// 若任务队列是空的,线程被阻塞
		while (pool->working_function_queue.empty()) {
			pthread_cond_wait(&(pool->notify), &(pool->lock));
		}

		// 工作函数队列中包含了需要执行的函数和函数参数,获取之。
		threadpool_tasks task = pool->working_function_queue.front();
		pool->working_function_queue.pop(); // 弹出队列

		// 线程释放互斥锁
		pthread_mutex_unlock(&(pool->lock));

		// 执行传入的函数。
		(*(task.function))(task.args);
		pthread_t pid = pthread_self();
		printf("my pid: %d\n", pid);
	}

	--pool->_working_thread_nums;
	pthread_mutex_unlock(&(pool->lock));
	pthread_exit(NULL);
	return NULL;
}

线程池

主要的数据成员:

  • 条件变量
  • 互斥锁
  • 任务队列,队列中储存的是包含函数指针和函数参数的结构体
  • 线程对象

主要的执行函数:

  • 线程主函数(需要声明为static,我暂时不知道为什么
  • 初始化线程池
  • 添加工作任务
// 定义函数结构,包含函数指针和函数参数指针
// 指针指向需要执行的函数和函数参数
typedef struct threadpool_tasks{
	void (*function)(void*);
	void* args;
};

class threadpool {
private:
	int _thread_nums;			// 总线程数
	int _working_thread_nums;	// 正在工作的线程数
	int _thread_tasks;	    // 线程任务数
	pthread_t* threads;	   // 线程变量
	pthread_mutex_t lock;  // 互斥锁 
	pthread_cond_t notify; // 条件变量
	queue<threadpool_tasks> working_function_queue; // 工作函数队列,参数为包含函数指针和函数参数指针的结构体

	static void* thread_working_function(void* args);

public:
	int init_threadpool(int thread_num);
	int add_workload(void (*function)(void*), void* args);
};

初始化线程池

函数逻辑:

  • 首先要初始化线程池的条件变量和互斥锁
  • 然后要为线程对象分配空间
  • 最后指定线程执行的主函数,创建线程
// 初始化线程池,运行用户指定数量的线程
int threadpool::init_threadpool(int thread_num) {
	do {
		// 初始化互斥锁和条件变量
		if (thread_num <= 0) break;
		if (pthread_mutex_init(&lock, NULL)) break;
		if (pthread_cond_init(&notify, NULL)) break;

		// 初始化线程数组
		threads = (pthread_t*)malloc(thread_num * sizeof(pthread_t));

		for (int i = 0; i < thread_num; ++i) {
			if (pthread_create(threads + i, NULL, thread_working_function, (void*)this) != 0) {
				// 未成功创建线程,销毁线程
				// destory() // 我没有实现这个函数
			}
			++_thread_nums;
			++_working_thread_nums;
		}
		return 0;
	} while (0);
	
	_thread_nums = 0;
	return -1; // 表示线程池未成功初始化
}

添加工作任务

函数逻辑:

  • 首先要先获取线程池的互斥锁(为什么?)
  • 然后将需要执行的函数加入队列中
  • 条件变量发出信号给线程
  • 释放互斥锁
// 添加线程工作函数,传入参数为:需要执行的函数,函数参数
int threadpool::add_workload(void (*function)(void*), void* args = nullptr) {
	// 先获取线程池的互斥锁
	if (pthread_mutex_lock(&lock) != 0)
		return -1;

	// 将工作函数加入队列
	threadpool_tasks task;
	task.function = function;
	task.args = args;
	
	working_function_queue.push(task);
	++_thread_tasks;
    
	// 发出信号,告诉线程池中的线程,可以进行工作了。
	if (pthread_cond_signal(&notify) != 0)
		return -2;

	// 释放互斥锁
	pthread_mutex_unlock(&lock);

	return 0;
}

​ 这个函数需要获取线程池的互斥锁,因为该函数体里面的语句要么完整的执行完所有,要么不执行。

​ 根据上面的实例来举例子的话,如果没有锁,可能队列push了一个任务,但_thread_tasks并没有自增,这会带来一些问题。

互斥锁的用法

pthread_mutex_init(&lock, NULL)初始化互斥锁。

pthread_mutex_lock(&lock)该条语句就可以锁住持有该锁的变量,在本例中也就是threadpool,保证线程一些原子操作的执行,比如线程池中内部数据结构的修改,线程池中的内部数据结构是所有线程共享的,因此需要使用互斥锁保证原子性。

pthread_mutex_unlock(&lock)释放互斥锁。

pthread_mutex_destroy(&lock, NULL)摧毁互斥锁。

条件变量的用法

pthread_cond_init(&notify, NULL)初始化条件变量。

pthread_cond_destroy(&notify, NULL)摧毁条件变量。

pthread_cond_wait(&notify, &lock)条件未满足,线程被条件变量阻塞,此时线程会释放互斥锁,当条件满足时,线程被唤醒,线程由阻塞态转变为运行态,该条语句会返回,此时会重新获取互斥锁。

pthread_cond_signal(&notify)唤醒至少一个阻塞在条件变量上的线程。

pthread_cond_broadcast(&notify)唤醒全部阻塞在条件变量上的线程。

​ 条件变量要搭配着互斥锁一起用。

条件变量的虚假唤醒

pthread_cond_signal(&notify)可能会唤醒多于一个线程,在唤醒的多个线程中,可能存在其中一个线程处理数据非常快的情况,使得其他线程没有数据可以进行处理,这些没有数据可以处理的线程就是被虚假唤醒。

​ 常见的bug处理,将if判断改为while判断,可以避免虚假唤醒的问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值