1. 概述
线程池就是提前创建了多个线程,这些线程被保存在所谓的"池子"中,同时还有一个任务队列,保存所有需要被执行的任务,然后通过调度,将任务分配给"池子"中的每一个线程进行执行,从而提高并发效率。
cartographer的线程池由两个类构成——
1、线程池 ThreadPool (src/cartographer/cartographer/common/thread_pool.h)
2、任务 Task (src/cartographer/cartographer/common/task.h)
结构图如下:
2. 关键问题
2.1 线程如何构建
线程池在构造函数中创建所有的线程:
// 根据传入的数字, 进行线程池的构造, DoWork()函数开始了一个始终执行的for循环
ThreadPool::ThreadPool(int num_threads) {
CHECK_GT(num_threads, 0) << "ThreadPool requires a positive num_threads!";
absl::MutexLock locker(&mutex_);
// std::vector<std::thread> pool_
for (int i = 0; i != num_threads; ++i) {
pool_.emplace_back([this]() { ThreadPool::DoWork(); });
}
}
线程启动后所运行的任务是 ThreadPool::DoWork()。
2.2 任务如何执行
每个独立的线程在各自的ThreadPool::DoWork()
中执行任务,首先去task_queue_
中取任务,cartographer的线程池一个比较有意思的设计是,考虑了任务的依赖性,把可以立即执行的任务放置到task_queue_
中,而存在依赖的任务放置到tasks_not_ready_
中。
2.3 任务的添加
任务的添加是在ThreadPool的Schedule函数
里完成的,不过会先将任务添加到tasks_not_ready_
中,然后在之后会判断该任务是否具有依赖,如果没依赖就会将该任务从tasks_not_ready_
提取到task_queue_
中,从而可以被正常执行。
2.4 任务依赖状态的转变
所谓的依赖也就是该任务需要等待另一个任务完成后才能执行,通过:
void Task::AddDependency(std::weak_ptr<Task> dependency)
添加依赖,然后 1、该任务的依赖数量+1(++uncompleted_dependencies_)
2、告知被依赖的任务 “完成后请通知我” (shared_dependency->AddDependentTask(this))
当该被依赖任务执行完毕后,会调用依赖该任务的每一个其他任务的 void Task::OnDependenyCompleted()函数 告知 “我已经完成”,此后,这些依赖该任务的其他任务各自的依赖数变量减1,当依赖数减为0时,调用void ThreadPool::NotifyDependenciesCompleted(Task* task),将该任务从线程池的tasks_not_ready_
移动到task_queue_
,从而可被正常调度执行,具体请看上面的结构图。