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

讲解关于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;
}

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

7、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);
  }
}

该函数同样比较简单,就是为 this 任务添加一个依赖任务 dependency,需要注意的是,这里仅仅对 this 的依赖任务进行了一个计数,然后为把 this 作为一个被依赖任务添加到 dependency 之中,其是通过 shared_dependency->AddDependentTask(this); 这段代码完成的。总的来说,就是 this 并没有记录其依赖项 dependency 指针对象,仅仅做了自增计数而已。但是会为 dependency 添加一个被依赖项 this。 这样做的目的是,dependency 任务完成之后可以通知 this。

8、Task::SetThreadPool()

该函数的主要功是将线程池与本任务连接起来, 如果没有未完成的依赖, 则告诉线程池可以将本任务放入到执行队列中。

// 将线程池与本任务连接起来, 如果没有未完成的依赖, 则告诉线程池可以将本任务放入到执行队列中
// 状态: NEW -> DISPATCHED || NEW -> DISPATCHED -> DEPENDENCIES_COMPLETED
void Task::SetThreadPool(ThreadPoolInterface* thread_pool) {
  absl::MutexLock locker(&mutex_);
  CHECK_EQ(state_, NEW);

  // 将任务状态设置为 DISPATCHED
  state_ = DISPATCHED;

  // 将thread_pool_to_notify_指针指向传入的thread_pool
  thread_pool_to_notify_ = thread_pool;

  // 如果本Task没有未完成的依赖, 则通知线程池可以将本任务放入到执行队列中
  if (uncompleted_dependencies_ == 0) {
    state_ = DEPENDENCIES_COMPLETED;
    CHECK(thread_pool_to_notify_);
    thread_pool_to_notify_->NotifyDependenciesCompleted(this);
  }
}

为一个任务设置线程池之后,首先会把改任务设置为 DISPATCHED 分发状态,然后判断该任务 this 是否存在依赖任务,如果没有,则通过 NotifyDependenciesCompleted() 把该任务添加到执行队列中去,等待线程执行。

9、Task::AddDependentTask()

该函数得主要目的是为把依赖于this任务的任务dependent_task记录到 this.dependent_tasks_ 变量中。首先其会判断一下本任务this是否处于完成COMPLETED状态,如果已经处于完成状态,则直接分发dependent_task,告诉线程池,可以执行依赖于this任务的任务dependent_task。

// 添加依赖本Task的Task
void Task::AddDependentTask(Task* dependent_task) {
  absl::MutexLock locker(&mutex_);

  // 如果本Task完成了, 那就通知依赖dependent_task
  if (state_ == COMPLETED) {
    dependent_task->OnDependenyCompleted();
    return;
  }
  // 将依赖本任务的任务放入set中
  bool inserted = dependent_tasks_.insert(dependent_task).second;
  CHECK(inserted) << "Given dependency is already a dependency.";
}

10、Task::OnDependenyCompleted()

首先只有任务状态为 NEW 或者 DISPATCHED 才能时,才能调用该函数。如果this.uncompleted_dependencies_ 为零,也就是说该任务已经没有依赖项了,同时this已经分发,则会通知线程池执行该任务。

// 本任务依赖的任务完成了, 可以将本任务加入到线程池的待处理列表中了
// 状态: DISPATCHED -> DEPENDENCIES_COMPLETED
void Task::OnDependenyCompleted() {
  absl::MutexLock locker(&mutex_);
  CHECK(state_ == NEW || state_ == DISPATCHED);
  // 依赖的任务减一
  --uncompleted_dependencies_;
  if (uncompleted_dependencies_ == 0 && state_ == DISPATCHED) {
    state_ = DEPENDENCIES_COMPLETED;
    CHECK(thread_pool_to_notify_);
    thread_pool_to_notify_->NotifyDependenciesCompleted(this);
  }
}
11、Task::Execute()

如果this调用该函数,则首先把任务认为标记为 RUNNING 状态,然后立即执行该函数,执行完成之后标记为 COMPLETED 状态,同时通过 OnDependenyCompleted() 通知所有依赖于该任务的任务。

// 执行本任务, 也就是传入的函数work_item_
// 状态: DEPENDENCIES_COMPLETED -> RUNNING -> COMPLETED
void Task::Execute() {
  {
    absl::MutexLock locker(&mutex_);
    CHECK_EQ(state_, DEPENDENCIES_COMPLETED);
    state_ = RUNNING;
  }

  // Execute the work item.
  if (work_item_) {
    work_item_();
  }

  absl::MutexLock locker(&mutex_);
  state_ = COMPLETED;

  // 通知依赖本任务的其他任务, 本任务执行完了
  for (Task* dependent_task : dependent_tasks_) {
    dependent_task->OnDependenyCompleted();
  }

 

四、thread_pool.cc

在了解了task.cc 文件之后,再来理解 thread_pool.cc 中的内容就十分简单了。

1、ThreadPoolInterface::Execute()
// 执行传入的 task的Execute()函数
void ThreadPoolInterface::Execute(Task* task) { task->Execute(); }
2、ThreadPoolInterface::SetThreadPool()

为认为task绑定一个线程池。

// 执行传入的 task的SetThreadPool()函数
void ThreadPoolInterface::SetThreadPool(Task* task) {
  task->SetThreadPool(this);
}
3、ThreadPool::ThreadPool()
// 根据传入的数字, 进行线程池的构造, DoWork()函数开始了一个始终执行的for循环
ThreadPool::ThreadPool(int num_threads) {
  CHECK_GT(num_threads, 0) << "ThreadPool requires a positive num_threads!";
  absl::MutexLock locker(&mutex_);
  for (int i = 0; i != num_threads; ++i) {
    pool_.emplace_back([this]() { ThreadPool::DoWork(); });
  }
}

创建 num_threads 个线程,每个线程都是调用 ThreadPool::DoWork() 函数,这些线程存储在 ThreadPool::pool_ 变量中。

4、ThreadPool::~ThreadPool()
// 只有等待 pool_ 结束所有的线程(join是等待直到线程结束),ThreadPool才能析构完成
ThreadPool::~ThreadPool() {
  {
    absl::MutexLock locker(&mutex_);
    CHECK(running_);
    running_ = false;
  }
  for (std::thread& thread : pool_) {
    thread.join();
  }
}
5、ThreadPool::NotifyDependenciesCompleted()
// task的依赖都结束了, 可以将task放入可执行任务的队列task_queue_中了
void ThreadPool::NotifyDependenciesCompleted(Task* task) {
  absl::MutexLock locker(&mutex_);

  // 找到task的索引
  auto it = tasks_not_ready_.find(task);
  CHECK(it != tasks_not_ready_.end());

  // 加入到任务队列中
  task_queue_.push_back(it->second);
  // 从未准备好的任务队列中删除task
  tasks_not_ready_.erase(it);
}
6、ThreadPool::Schedule()
// 将task插入到tasks_not_ready_队列中, 并执行task的SetThreadPool()函数
std::weak_ptr<Task> ThreadPool::Schedule(std::unique_ptr<Task> task) {
  std::shared_ptr<Task> shared_task;
  {
    absl::MutexLock locker(&mutex_);
    auto insert_result =
        tasks_not_ready_.insert(std::make_pair(task.get(), std::move(task)));

    // map::insert() 会返回pair<map::iterator,bool> 类型, 
    // 第一个值为迭代器, 第二个值为插入操作是否成功
    CHECK(insert_result.second) << "Schedule called twice";
    shared_task = insert_result.first->second;
  }
  SetThreadPool(shared_task.get());
  return shared_task;
}
7、ThreadPool::DoWork()

// 开始一个不停止的for循环, 如果任务队列不为空, 就执行第一个task
void ThreadPool::DoWork() {
#ifdef __linux__
  // This changes the per-thread nice level of the current thread on Linux. We
  // do this so that the background work done by the thread pool is not taking
  // away CPU resources from more important foreground threads.
  CHECK_NE(nice(10), -1);
#endif

  const auto predicate = [this]() EXCLUSIVE_LOCKS_REQUIRED(mutex_) {
    return !task_queue_.empty() || !running_;
  };

  // 始终执行, 直到running_为false时停止执行
  for (;;) {
    std::shared_ptr<Task> task;
    {
      absl::MutexLock locker(&mutex_);
      mutex_.Await(absl::Condition(&predicate));

      // map_builder.lua中设置的线程数, 4个线程处理同一个task_queue_
      // 如果任务队列不为空, 那就取出第一个task
      if (!task_queue_.empty()) {
        task = std::move(task_queue_.front());
        task_queue_.pop_front();
      } else if (!running_) {
        return;
      }
    }
    CHECK(task);
    CHECK_EQ(task->GetState(), common::Task::DEPENDENCIES_COMPLETED);

    // 执行task
    Execute(task.get());
  }
}

 

五、总结

了解Task与ThreadPool这两个类之后,再回到 pose_graph_2d.cc 文件中的 PoseGraph2D::AddNode(), 其最后可以看到如下一段代码:

  // 把计算约束的工作放入workitem中等待执行
  AddWorkItem([=]() LOCKS_EXCLUDED(mutex_) {
    return ComputeConstraintsForNode(node_id, insertion_submaps,
                                     newly_finished_submap);
  });

看起来比较简单,就是构建了计算约束的函数然后调用 PoseGraph2D::AddWorkItem() 函数把该任务抛给线程池执行。但是有这么一个问题,线程池 ThreadPool 会运行 num_threads 个线程执行 ThreadPool::DoWork() 函数,都是处理 ThreadPool::task_queue_ 中的任务。

但是源码中,并没有把计算约束的任务直接添加到队列 ThreadPool::task_queue_ 中,而是另外创建了新的队列 PoseGraph2D::work_queue_,然后把计算约束的任务添加到该队列中。那么如何让线程池处理 PoseGraph2D::work_queue_ 队列中的任务呢?

思路是把 PoseGraph2D::DrainWorkQueue() 函数封装成一个 Task 对象,然后添加至线程池 ThreadPool::task_queue_ 中,由一个线程执行。也就是说,只要 work_queue_ 存在数据,num_threads 中就有一个线程循环调用 PoseGraph2D::DrainWorkQueue() 函数处理 work_queue_ 队列。该逻辑在 PoseGraph2D::AddWorkItem() 中可体现。

  if (work_queue_ == nullptr) {
    // work_queue_的初始化
    work_queue_ = absl::make_unique<WorkQueue>();
    // 将 执行一次DrainWorkQueue()的任务 放入线程池中等待计算
    auto task = absl::make_unique<common::Task>();
    task->SetWorkItem([this]() { DrainWorkQueue(); });
    thread_pool_->Schedule(std::move(task));
  }

分析 PoseGraph2D::DrainWorkQueue() 函数可知道, 如果 work_queue_ 为空了,该函数会 return 返回,表示执行完成。

如果依旧存在疑惑的朋友,可以结合前面的博客 (02)Cartographer源码无死角解析-(59) 2D后端优化→ 线程池: DrainWorkQueue()、AddWorkItem() 进行分析。

 
 
 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

江南才尽,年少无知!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值