(02)Cartographer源码无死角解析-(60) 2D后端优化→ 线程池

讲解关于slam一系列文章汇总链接:史上最全slam从零开始,针对于本栏目讲解(02)Cartographer源码无死角解析-链接如下:
(02)Cartographer源码无死角解析- (00)目录_最新无死角讲解:https://blog.csdn.net/weixin_43013761/article/details/127350885
 
文末正下方中心提供了本人 联系方式, 点击本人照片即可显示 W X → 官方认证 {\color{blue}{文末正下方中心}提供了本人 \color{red} 联系方式,\color{blue}点击本人照片即可显示WX→官方认证} 文末正下方中心提供了本人联系方式,点击本人照片即可显示WX官方认证
 

一、前言

上一篇博客中,以 DrainWorkQueue()、AddWorkItem() 为例,讲解了线程池的一个应用,或许从这个例子来看,线程池似乎很简单的,其实不然。Cartographer 线程池的相关设计是比较复杂的,其主要涉及到如下两个文件对应的类:

Task: src/cartographer/cartographer/common/task.cc 
ThreadPool: src/cartographer/cartographer/common/thread_pool.cc

后续的讲解也是围绕着这两个类,不过 ThreadPool 先对来说比较简单,下面先要分析的是 Task。总的来说 Task 可是说是一个任务调配系统,或者说机制。

Cartographer 中很多任务都存在依赖关系,具体例子后续分析源码再讲解。这里列举一个比较简单的示例:比如存在任务2,记为 t a s k 2 3 , 4 1 task2_{3,4}^{1} task23,41,其表示的含义为任务2依赖于任务3与任务4,也就是说要执行任务2必须先执行完任务3与任务4,同时task2是task1的依赖,即执行任务1之前必须先执行任务2。现在假设有三个任务 t a s k 1 2 , 3 , 5 task1_{2,3,5} task12,3,5 t a s k 2 6 , 7 1 task2^{1}_{6,7} task26,71 t a s k 3 1 task3^1 task31 t a s k 5 1 task5^1 task51

t a s k 1 2 , 3 , 5 \color{blue}{task1_{2,3,5}} task12,3,5: 该为目标任务1,其依赖任务 2,3,5,但是没有任何任务依赖该任务1。即只要执行完任务 2,3,5 即可执行该任务1。

t a s k 2 6 , 7 1 \color{blue}{task2^{1}_{6,7}} task26,71:任务1依赖于该任务,同时该任务依赖于任务6、7。只有执行完任务6,7之后才会执行该任务,通知会通知系统执行任务1。

t a s k 3 1 \color{blue}{task3^1} task31:该任务3不依赖于任何任务,可以直接执行,执行完之后会通知系统执行任务1。

t a s k 5 1 \color{blue}{task5^1} task51:该任务5不依赖于任何任务,可以直接执行,执行完之后会通知系统执行任务1。

为了方便后续示例讲解,大家先熟悉上面的书写方式,后续看文章更加轻松,从上面的标识可以猜出,Cartographer 的任务之间存在依赖与被依赖的关系,可以说是错综复杂的。除此之外,对于每个任务都有状态标识,即成员变量 Task::state_。 共有如下几种状态:

enum State {
    
     NEW, DISPATCHED, DEPENDENCIES_COMPLETED, RUNNING, COMPLETED };
  /**
    NEW:新建任务, 还未schedule到线程池
    DISPATCHED: 任务已经schedule 到线程池
    DEPENDENCIES_COMPLETED: 任务依赖已经执行完成
    RUNNING: 任务执行中
    COMPLETED: 任务完成

    对任一个任务的状态转换顺序为:
    NEW->DISPATCHED->DEPENDENCIES_COMPLETED->RUNNING->COMPLETED
  */

明白了 Task 的核心思想之后,再来分析其代码会简单很多,难点主要在于任务的调度上面。
 

二、task.h

首先来看看头文件,关于任务状态,即成员变量 Task::State 上面已经讲解,具体的用法后续随函数一起分析。另外还通过 std::function<void()> 定义了一个可调用对象指针类型,该类型无参数无返回值。接着翻到最后可以看到定义了如下成员变量:

  // 需要执行的任务
  WorkItem work_item_ GUARDED_BY(mutex_);
  ThreadPoolInterface* thread_pool_to_notify_ GUARDED_BY(mutex_) = nullptr;
  // 初始状态为NEW
  State state_ GUARDED_BY(mutex_) = NEW;
  // 本任务依赖的任务的个数
  unsigned int uncompleted_dependencies_ GUARDED_BY(mutex_) = 0;
  // 依赖本任务的其他任务
  std::set<Task*> dependent_tasks_ GUARDED_BY(mutex_);

  absl::Mutex mutex_;

work_item_ 后续会与对应的工作项绑定,通常是一个 lambda 表达式,即后续通过 work_item_() 形式即可进行调用。thread_pool_to_notify_ 为指向线程池的指针,在有必要的时候会通知其指向的线程池。比如 t a s k 3 1 {task3^1} task31 任务完成时,就有可能会通知线程池执行任务1。

uncompleted_dependencies_ 记录的是本任务依赖的个数,如前面的示例 t a s k 1 2 , 3 , 5 task1_{2,3,5} task12,3,5 表示其依赖任务数为3,分别为【任务2、任务3、任务5】。dependent_tasks_ 作用是相反,且是一个指针集合,其指向的是依赖于本任务的其他任务,如 t a s k 2 6 , 7 1 , 3 task2^{1,3}_{6,7} task26,71,3 对应的 dependent_tasks_ 就包含【任务1,任务3】的指针。最后就是还有成员变量 mutex_,该就比较简单了,为一个锁,后续用到很容易理解的。

核心 \color{red} 核心 核心 根据上面的分析,不难猜到,其至少需要实现两个函数:Task::AddDependency()→ 为任务添加依赖任务;Task::AddDependentTask()→记录依赖于本任务的任务,即被依赖项。那么就来看看这些函数吧。
 

三、task.cc

1、Task::~Task()

该函数简单,也就是每个 Task 对象析构的时候,都要保证是完成状态,否则会打印一些信息,告知这个任务处于分发与完成之间却被删除了。

Task::~Task() {
    
    
  // TODO(gaschler): Relax some checks after testing.
  if (state_ != NEW && state_ != COMPLETED) {
    
    
    LOG(WARNING) << "Delete Task between dispatch and completion.";
  }
}

2、Task::GetState()

获取当前任务的状态,这里会上锁,防止访问期间 Task::state_ 会发生改变。

// 返回本Task当前状态 
Task::State Task::GetState() {
    
    
  absl::MutexLock locker(&mutex_);
  return state_;
}

3、Task::GetState()

// 设置本Task需要执行的任务 (函数) 
// 状态: NEW
void Task::SetWorkItem(const WorkItem& work_item) {
    
    
  absl::MutexLock locker(&mutex_);
  CHECK_EQ(state_, NEW);
  work_item_ = work_item;
}

为该任务设置工作内容,只有状态为 NEW 的任务才能设置,或者重新绑定工作内容。

4、Task::AddDependency()

// 为本任务添加依赖
void Task::AddDependency(std::weak_ptr<Task> dependency) {
    
    
  std::shared_ptr<Task> shared_dependency;
  {
    
    
    absl::MutexLock locker(&mutex_);
    CHECK_EQ(state_, NEW);
    // 如果指针指针成功获取对象
    if ((shared_dependency = dependency.lock())) {
    
    
      ++uncompleted_dependencies_;
    }
  }
  
  if (shared_dependency) {
    
    
    // 将本task加入到shared_dependency的集合dependent_tasks_中
    shared_dependency->AddDependentTask(this);
  }
}

首先要注意的就是传入的参数,其是指向 Task 类型弱指针,弱指针的具体细节这里就不进行描述了,大致如下:

// 可以从一个shared_ptr或者另一个weak_ptr对象构造, 获得资源的观测权
// 但weak_ptr没有共享资源, 它的构造不会引起指针引用计数的增加.
// 同样, 在weak_ptr析构时也不会导致引用计数的减少, 它只是一个静静地观察者.
// weak_ptr没有重载operator*和->, 这是特意的, 因为它不共享指针, 不能操作资源, 这是它弱的原因
// 但它可以使用一个非常重要的成员函数lock()从被观测的shared_ptr获得一个可用的shared_ptr对象, 从而操作资源.

该函数首先会创建一个 Task 类型的共享指针 shared_dependency,然后上锁,判断当前任务的状态是否为NEW,只有新建的任务,才能为该任务添加依赖项。根据传入的弱指针 dependency,尝试获取共享指针赋值给 shared_dependency,如果能够获取到,说明其依赖任务存在,所以执行 ++uncompleted_dependencies_。

同时还要把本任务 this 作为被依赖添加到依赖任务 shared_dependency 中。这里举一个例子,假设任务 this 为 t a s k 2 task2 task2,如果其依赖于任务4,那么 this 应该记为 t a s k 2 4 task2_4 task24,那么显然任务4应该记为 t a s k 4 2 task4^2 task42,因为任务2体耐于任务4。代码 shared_dependency->AddDependentTask(this) 就是由 t a s k 4 task4 task4 t a s k 4 2 task4^2 task42 的过程。

总结:本任务在添加依赖任务的时候,并没有保存依赖任务的的指针(但是记录依赖任务的数量),而是对依赖任务进行操作,把本任务作为被依赖任务添加至依赖任务中。

5、Task::GetState()

// 返回本Task当前状态 
Task::State Task::GetState() {
    
    
  absl::MutexLock locker(&mutex_);
  return state_;
}

该函数就比较简单了,就是返回任务的状态,前面提到过其包含: NEW, DISPATCHED, DEPENDENCIES_COMPLETED, RUNNING, COMPLETED。

6、Task::SetWorkItem()

// 设置本Task需要执行的任务 (函数) 
// 状态: NEW
void Task::SetWorkItem(const WorkItem& work_item) {
    
    
  absl::MutexLock locker(&mutex_);
  CHECK_EQ(state_, NEW);
  work_item_ = work_item;
}

该函数也比较简单,就是上锁然后设置任务的工作内容,通常为一个无放回值的函数指针,后续会执行这个函数。

 
 
 

猜你喜欢

转载自blog.csdn.net/weixin_43013761/article/details/129385753