WebRTC源码分析-TaskQueue(任务队列)-TaskQueueStdlib

1. 前言

TaskQueueStdlib类是WebRTC任务队列机制的核心类,也是整个任务队列的标准库,在阅读本文之前,需要对TaskQueueBase类有一定了解。
可以参考这篇文章WebRTC源码分析-TaskQueue(任务队列)-TaskQueueBase
WebRTC版本:M84

2. 正文

2.0. 预说明:TaskQueuePriorityToThreadPriority

rtc::ThreadPriority TaskQueuePriorityToThreadPriority(
    TaskQueueFactory::Priority priority) {
  switch (priority) {
    case TaskQueueFactory::Priority::HIGH:
      return rtc::kRealtimePriority;
    case TaskQueueFactory::Priority::LOW:
      return rtc::kLowPriority;
    case TaskQueueFactory::Priority::NORMAL:
      return rtc::kNormalPriority;
    default:
      RTC_NOTREACHED();
      return rtc::kNormalPriority;
  }
}

此函数用于将任务队列的优先级转换为线程的优先级。
TaskQueueFactory::Priority定义如下:

enum class Priority { NORMAL = 0, HIGH, LOW };

2.1. TaskQueueStdlib

path:
rtc_base\task_queue_stdlib.h
rtc_base\task_queue_stdlib.cc

2.1.1. 类声明

class TaskQueueStdlib final : public TaskQueueBase {
 public:
  TaskQueueStdlib(absl::string_view queue_name, rtc::ThreadPriority priority);
  ~TaskQueueStdlib() override = default;

  void Delete() override;
  void PostTask(std::unique_ptr<QueuedTask> task) override;
  void PostDelayedTask(std::unique_ptr<QueuedTask> task,
                       uint32_t milliseconds) override;

 private:
  using OrderId = uint64_t;

  struct DelayedEntryTimeout {
    int64_t next_fire_at_ms_{};
    OrderId order_{};

    bool operator<(const DelayedEntryTimeout& o) const {
      return std::tie(next_fire_at_ms_, order_) <
             std::tie(o.next_fire_at_ms_, o.order_);
    }
  };

  struct NextTask {
    bool final_task_{false};
    std::unique_ptr<QueuedTask> run_task_;
    int64_t sleep_time_ms_{};
  };

  NextTask GetNextTask();
  static void ThreadMain(void* context);
  void ProcessTasks();
  void NotifyWake();
  rtc::Event started_;
  rtc::Event stopped_;
  rtc::Event flag_notify_;
  rtc::PlatformThread thread_;
  rtc::CriticalSection pending_lock_;
  bool thread_should_quit_ RTC_GUARDED_BY(pending_lock_){false};
  OrderId thread_posting_order_ RTC_GUARDED_BY(pending_lock_){};
  std::queue<std::pair<OrderId, std::unique_ptr<QueuedTask>>> pending_queue_
      RTC_GUARDED_BY(pending_lock_);
  std::map<DelayedEntryTimeout, std::unique_ptr<QueuedTask>> delayed_queue_
      RTC_GUARDED_BY(pending_lock_);
};

可以看出TaskQueueStdlibpublic成员除构造函数和析构函数之外只有三个函数,private成员都是为了实现public方法而定义的,所以后续会先分析其私有成员,最后再分析三个public成员函数的具体实现方式。

2.1.2. 私有成员实现

此处使用using语句,类似于typedef,类型OrderId实际上就是uint64_t

2.1.2.1. 私有数据成员
// 表示该线程是否已经开始
rtc::Event started_;
// 表示该线程是否已经结束
rtc::Event stopped_;
// 每当有新的任务等待时,就会发出信号
rtc::Event flag_notify_;
// 表示被分配用于处理任务的活动工作线程
rtc::PlatformThread thread_;
// 对于存在多个线程访问的数据需要上锁
rtc::CriticalSection pending_lock_;
// 表示工作线程是否需要现在关闭
bool thread_should_quit_ RTC_GUARDED_BY(pending_lock_){false};
// 保存下一个任务的序号,用于将其放入一个挂起的队列中
OrderId thread_posting_order_ RTC_GUARDED_BY(pending_lock_){};
// 需要在工作线程中按照先进先出的队列顺序处理的待办任务列表
std::queue<std::pair<OrderId, std::unique_ptr<QueuedTask>>> pending_queue_
    RTC_GUARDED_BY(pending_lock_);
// 需要在工作线程中延迟一段时间再处理的待办任务列表,如果两个任务在相同的时间间隔内
// 发生,那么将根据先后顺序进行处理。
std::map<DelayedEntryTimeout, std::unique_ptr<QueuedTask>> delayed_queue_
    RTC_GUARDED_BY(pending_lock_);
2.1.2.2. 私有成员类&结构
struct DelayedEntryTimeout {
  int64_t next_fire_at_ms_{};
  OederId order_{};
  
  bool operator<(const DelayedEntryTimeout& o) const {
    return std::tie(next_fire_at_ms_, order_) < 
           std::tie(o.next_fire_at_ms_, o.order_);
  }
};

DelayedEntryTimeout具有类似于时间戳的功能,next_fire_at_ms_记录了任务执行的绝对时间,OrderId则记录了任务对应的序号,重载了<操作符用于比较两个
DelayedEntryTimeout,该属性较小的任务会先被执行。
DelayedEntryTimeout比较规则:先比较next_fire_at_ms_,如果不等直接返回对应的bool值;如果相等,则再次比较order,返回比较结果对应的bool值。

struct NextTask {
  bool final_task_{false};
  std::unique_ptr<QueuedTask> run_task_;
  int64_t sleep_time_ms_{};
};

NextTask用于表示下一个任务,前两个属性很容易理解,第三个属性是为了兼容DelayedTask而设置的,具体的实现将会在GetNextTask函数中介绍到。

2.1.2.3. 私有成员函数实现
2.1.2.3.1. GetNextTask
TaskQueu1Stdlib::NextTask TaskQueueStdlib::GetNextTask() {
  NextTask result{};
  // 获取当前的绝对时间(ms)
  auto tick = rtc::TimeMillis();

  rtc::CritScope lock(&pending_lock_);
  // 判断是否线程需要退出
  if (thread_should_quit_) {
    result.final_task_ = true;
    return result;
  }
  // 如果延迟任务队列非空,则首先从其中取出任务 
  if (delayed_queue_.size() > 0) {
    // 获取延迟任务相关信息
    auto delayed_entry = delayed_queue_.begin();
    const auto& delay_info = delayed_entry->first;
    auto& delay_run = delayed_entry->second;
    // 判断是否应该执行延迟任务
    if (tick >= delay_info.next_fire_at_ms_) {
      // 如果即时任务队列非空,通过比较决定取出哪种任务
      if (pending_queue_.size() > 0) {
        auto& entry = pending_queue_.front();
        auto& entry_order = entry.first;
        auto& entry_run = entry.second;
        // 如果即时任务序号较小,则直接返回该即时任务 
        if (entry_order < delay_info.order_) {    
          result.run_task_ = std::move(entry_run);              //<--1
          pending_queue_.pop();
          return result;
        }
      }
      // 如果即时任务队列为空,或者1处即时任务序号较大,则取出最靠前的延迟任务
      result.run_task_ = std::move(delay_run);                  //<--2
      delayed_queue_.erase(delayed_entry);
      return result;
    }
    // !!如果运行到这里,此时result.task_run_为nullptr,表明没有任务需要处理
    // 更新sleep_time_ms_
    result.sleep_time_ms_ = delay_info.next_fire_at_ms_ - tick; //<--3
  }
  // 延迟任务队列为空,即时任务队列非空
  if (pending_queue_.size() > 0) {       
    auto& entry = pending_queue_.front();                       //<--4
    result.run_task_ = std::move(entry.second);
    pending_queue_.pop();
  }
  return result;
}

其中涉及到mapqueue两个容器的相关函数,可以自行查阅。
由于NextTask结构体内部的run_task_unique_ptr,所以不能直接赋值而是使用std::move转移所有权。

GetNextTask函数体较长,逻辑略显复杂,而且非常重要,一定要理清楚如何从任务队列中取出任务。
代码块里做了四处标记,便于理解整个流程(此处分析认为thread_should_quit_true):

if 延迟任务队列非空
  if 需要执行下一个延迟任务
    if 即时任务队列非空
      if 即时任务序号较小
        返回即时任务(1)
      else 
        返回延迟任务(2)
    else
      返回延迟任务(2)
  else
    更新result.sleep_time_ms_(3)
if 即时任务队列非空
  result.run_task_设置为即时任务(4)
返回result

这一部分可以概括为:

  • result.run_task_为空,result.sleep_time_ms_为0时:两个任务队列均为空
  • result.run_task_为空,result.sleep_time_ms_不为0时:即时任务队列为空,延迟任务队列非空,但是没有可执行的延迟任务
  • result.run_task_不为空,result.sleep_time_ms_为0时:即时任务队列非空,延迟任务队列为空
  • result.run_task_不为空,result.sleep_time_ms_不为0时:即时任务队列非空,延迟任务队列非空,但是没有可执行的延迟任务
2.1.2.3.2 ThreadMain
// static
void TaskQueueStdlib::ThreadMain(void* context) {
  TaskQueueStdlib* me = static_cast<TaskQueueStdlib*>(context);
  CurrentTaskQueueSetter set_current(me);
  me->ProcessTasks();
}

ThreadMain是任务处理线程真正的入口函数,其首先将传入的参数强制转换成TaskQueueStdlib*,然后将这个任务队列注册到当前的线程中,随后开始处理任务。
注意此函数是一个static函数。

2.1.2.3.3. ProcessTasks
void TaskQueueStdlib::ProcessTasks() {
  started_.Set();
  while (true) {
    auto task = GetNextTask();
    if (task.final_task_)
      break;
    if (task.run_task_) {
      // release()会解除智能指针对这个QueuedTask的占用,
      // 并将该智能指针置空
      QueuedTask* release_ptr = task.run_task_.release();
      if (release_ptr->Run())
        delete release_ptr;

      // 取出下一个任务
      continue;
    }
    // task.sleep_time_ms_为0时表示
    if (0 == task.sleep_time_ms_)
      flag_notify_.Wait(rtc::Event::kForever);
    else
      flag_notify_.Wait(task.sleep_time_ms_);
  }
  stopped_.Set();
}

ProcessTasks函数与GetNextTask函数协同工作。
GetNextTask函数部分的分析可知只要task.run_task_为空,就说明即时任务队列为空且暂时没有需要执行的任务,但此时又面临着两种情况:

  • 如果延迟任务队列为空,那么直接睡眠直到被唤起
  • 如果延迟任务队列非空,那么将会睡眠指定时间
2.1.2.3.4. NotifyWake
void TaskQueueStdlib::NotifyWake() {
  flag_notify_.Set();
}

任务队列中存放着待执行的任务:

  • 对于即时任务,线程会忙于执行该任务而不会等待flag_notify_事件。
  • 如果没有即时任务,但有一个延迟任务正在等待,那么线程将会等待flag_notify_事件,也就是ProcessTasks中所提及到的flag_notify_.Wait(task.sleep_time_ms_);
  • 如果即时任务队列和延迟任务队列都为空,那么线程将无限期等待flag_notify_事件,直到有一个信号显示有新的任务被添加(或者告诉线程需要终止)。

任何情况下,当一个新的上述请求被添加后,会发出flag_notify_信号。如果此时线程正在等待,则会被立即唤醒并且重新评估下一步需要做什么。如果线程并没有在等待,那么线程将保持信号,在下一次试图等待flag_notify_事件发生时被唤醒。

在发出flag_notify_信号来唤醒可能正在睡眠的线程之前,需要确保有任务或相关请求添加到队列中,从而避免竞争情况:线程被通知唤醒但是发现没有任务需要执行,所以会再次等待信号,然而这样的信号将有可能不会再次出现。

2.1.3. 公有成员实现

2.1.3.1. 公有成员函数实现
2.1.3.1.1. TaskQueueStdlib
TaskQueueStdlib::TaskQueueStdlib(absl::string_view queue_name,
                                 rtc::ThreadPriority priority)
    : started_(/*manual_reset=*/false, /*initially_signaled=*/false),
      stopped_(/*manual_reset=*/false, /*initially_signaled=*/false),
      flag_notify_(/*manual_reset=*/false, /*initially_signaled=*/false),
      thread_(&TaskQueueStdlib::ThreadMain, this, queue_name, priority) {
  thread_.Start();
  started_.Wait(rtc::Event::kForever);
}

创建一个任务处理线程,随后开始执行入口函数并挂起当前线程。
当任务处理线程准备就绪之后,会唤醒当前线程。
当前线程负责创建一个任务处理线程并且向其投递任务,任务处理线程负责处理任务队列内的任务。

2.1.3.1.2.Delete
void TaskQueueStdlib::Delete() {
  RTC_DCHECK(!IsCurrent());

  {
    rtc::CritScope lock(&pending_lock_);
    thread_should_quit_ = true;
  }

  NotifyWake();

  stopped_.Wait(rtc::Event::kForever);
  thread_.Stop();
  delete this;
}

此函数用于销毁任务队列对象:

  • 首先判断当前线程是不是任务处理线程,因为销毁操作不可以在任务处理线程中进行
  • 随后标记任务处理线程需要退出并唤醒线程执行相关任务
  • 然后当前线程等待任务处理线程退出后唤醒主线程
  • 主线程回收任务处理线程,最后释放任务队列对象
2.1.3.1.3. PostTask
void TaskQueueStdlib::PostTask(std::unique_ptr<QueuedTask> task) {
  {
    rtc::CritScope lock(&pending_lock_);
    // 序号自增赋值
    OrderId order = thread_posting_order_++;
    // 加入到即时任务队列中
    pending_queue_.push(std::pair<OrderId, std::unique_ptr<QueuedTask>>(
        order, std::move(task)));
  }
  // 唤醒任务处理线程处理任务
  NotifyWake();
}
2.1.3.1.4. PostDelayedTask
void TaskQueueStdlib::PostDelayedTask(std::unique_ptr<QueuedTask> task,
                                      uint32_t milliseconds) {
  // 计算延迟任务的执行时间(绝对时间)
  auto fire_at = rtc::TimeMillis() + milliseconds;

  DelayedEntryTimeout delay;
  // 设置延迟任务的执行时间
  delay.next_fire_at_ms_ = fire_at;

  {
    rtc::CritScope lock(&pending_lock_);
    // 序号自增赋值
    delay.order_ = ++thread_posting_order_;
    delayed_queue_[delay] = std::move(task);
  }
  // 唤醒任务处理线程处理任务
  NotifyWake();
}

3. 总结

TaskQueueStdlib涉及到任务队列的核心实现方式,尤其是GetNextTaskProcessTasks两个函数,需要理清相关的逻辑,从而了解整个模块的流程。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值