简易线程池的实现
参考资料:
互斥锁作用的理解
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(¬ify, 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(¬ify) != 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(¬ify, NULL)
初始化条件变量。
pthread_cond_destroy(¬ify, NULL)
摧毁条件变量。
pthread_cond_wait(¬ify, &lock)
条件未满足,线程被条件变量阻塞,此时线程会释放互斥锁,当条件满足时,线程被唤醒,线程由阻塞态转变为运行态,该条语句会返回,此时会重新获取互斥锁。
pthread_cond_signal(¬ify)
唤醒至少一个阻塞在条件变量上的线程。
pthread_cond_broadcast(¬ify)
唤醒全部阻塞在条件变量上的线程。
条件变量要搭配着互斥锁一起用。
条件变量的虚假唤醒
pthread_cond_signal(¬ify)
可能会唤醒多于一个线程,在唤醒的多个线程中,可能存在其中一个线程处理数据非常快的情况,使得其他线程没有数据可以进行处理,这些没有数据可以处理的线程就是被虚假唤醒。
常见的bug处理,将if
判断改为while
判断,可以避免虚假唤醒的问题。