从base/task_scheduler/task_traits.h中的枚举量TaskShutdownBehavior可以看到,chromium针对投递的task在浏览器退出时应该表现的行为分为三类,CONTINUE_ON_SHUTDOWN、SKIP_ON_SHUTDOWN、BLOCK_SHUTDOWN。值得一提的是base::SequencedWorkerPool::WorkerShutdown中同样声明了三个一样的enum,一般我们在开发过程中应该尽量避免这种同样的常量在不同的位置定义两次的行为,因为很可能代码在不同的人手上改着改着这两份东西就不一样了,这样就埋了个深坑。如果实在万不得已的情况下只能定义两份,那就学习一下chromium的做法吧,用一个static_assert保证两份定义的值是一样的:
bool SequencedWorkerPool::Inner::PostTaskToTaskScheduler(...) {
...
static_assert(
static_cast<int>(TaskShutdownBehavior::CONTINUE_ON_SHUTDOWN) ==
static_cast<int>(CONTINUE_ON_SHUTDOWN),
"TaskShutdownBehavior and WorkerShutdown enum mismatch for "
"CONTINUE_ON_SHUTDOWN.");
...
然后先看一下这三种分类到底是几个意思,其实代码里已经有了非常详细的注释。简单来说大概就是这样:
- CONTINUE_ON_SHUTDOWN类型的任务不会影响退出流程,它跟退出流程各走各的,所以可能任务执行的时候退出流程已经走得七七八八了,所以很多单例啊全局对象啊之类的已经析构了,这个要比较注意;
- SKIP_ON_SHUTDOWN类型的任务,在开始退出流程前已经在执行的会继续执行且执行时阻塞退出流程,否则会被忽略;
- BLOCK_SHUTDOWN类型的任务,不论在退出流程开始前后投递都会被执行,且执行时阻塞退出流程。换句话说就是这些任务执行完前退出流程会等在那里。
那么这个又是怎么实现的呢?
首先,在向线程池投递的每个任务都会由TaskTracker来跟踪,在正式加入线程池的任务队列之前会经过TaskTracker::WillPostTask,如果返回false则不会被加入队列(是否已经开始退出流程是在TaskTracker::State里面,在一个int值的最低位存的,估计是跟任务数量合在一个变量里面存有助于用原子操作优化吧):
bool TaskTracker::WillPostTask(const Task* task) {
if (!BeforePostTask(task->traits.shutdown_behavior()))
return false;
……
bool TaskTracker::BeforePostTask(TaskShutdownBehavior shutdown_behavior) {
if (shutdown_behavior == TaskShutdownBehavior::BLOCK_SHUTDOWN) {
// BLOCK_SHUTDOWN tasks block shutdown between the moment they are posted
// and the moment they complete their execution.
const bool shutdown_started = state_->IncrementNumTasksBlockingShutdown();
if (shutdown_started) {
AutoSchedulerLock auto_lock(shutdown_lock_);
// A BLOCK_SHUTDOWN task posted after shutdown has completed is an
// ordering bug. This aims to catch those early.
DCHECK(shutdown_event_);
if (shutdown_event_->IsSignaled()) {
……
return false;
}
……
}
return true;
}
// A non BLOCK_SHUTDOWN task is allowed to be posted iff shutdown hasn't
// started.
return !state_->HasShutdownStarted();
}
可以看出,在开始退出流程之后,除了BLOCK_SHUTDOWN类型的任务就不会再被加入任务队列了;
同样,在开始执行任务之前也有类似的判断,这里BeforeRunTask里的代码就不贴了:
bool TaskTracker::RunTask(std::unique_ptr<Task> task,
const SequenceToken& sequence_token) {
DCHECK(task);
DCHECK(sequence_token.IsValid());
const TaskShutdownBehavior shutdown_behavior =
task->traits.shutdown_behavior();
const bool can_run_task = BeforeRunTask(shutdown_behavior);
const bool is_delayed = !task->delayed_run_time.is_null();
if (can_run_task) {
PerformRunTask(std::move(task), sequence_token);
AfterRunTask(shutdown_behavior);
}
...
最后,退出流程是在哪里进行的呢?始于TaskTracker::PerformShutdown,由于某些任务会阻塞退出流程,在它们执行完成之前需要等待,所以这里用了一个Event对象进行同步:
void TaskTracker::PerformShutdown() {
{
AutoSchedulerLock auto_lock(shutdown_lock_);
shutdown_event_.reset(
new WaitableEvent(WaitableEvent::ResetPolicy::MANUAL,
WaitableEvent::InitialState::NOT_SIGNALED));
const bool tasks_are_blocking_shutdown = state_->StartShutdown();
if (!tasks_are_blocking_shutdown) {
shutdown_event_->Signal();
return;
}
}
// It is safe to access |shutdown_event_| without holding |lock_| because the
// pointer never changes after being set above.
{
base::ThreadRestrictions::ScopedAllowWait allow_wait;
shutdown_event_->Wait();
}
...
}
TaskTracker::State里用一个int值的最低位存放是否已经开始退出流程,其他位存放阻塞类型任务的数量(对这个计数的维护可以在TaskTracker中查找IncrementNumTasksBlockingShutdown与DecrementNumTasksBlockingShutdown的调用),在TaskTracker::StartShutdown里面会将退出状态标志位置位,并判断目前是否有阻塞类型任务。如果没有,则可以直接结束退出流程;否则需要等待这些任务完成。可以看一下StartShutdown里面的实现:
class TaskTracker::State {
// Sets a flag indicating that shutdown has started. Returns true if there are
// tasks blocking shutdown. Can only be called once.
bool StartShutdown() {
const auto new_value =
subtle::NoBarrier_AtomicIncrement(&bits_, kShutdownHasStartedMask);
// Check that the "shutdown has started" bit isn't zero. This would happen
// if it was incremented twice.
DCHECK(new_value & kShutdownHasStartedMask);
const auto num_tasks_blocking_shutdown =
new_value >> kNumTasksBlockingShutdownBitOffset;
return num_tasks_blocking_shutdown != 0;
}
…
}
那么如果需要等待其他任务完成的话,最终这些任务执行完后会在哪里重新唤醒退出流程呢?每个任务在执行前会经过TaskTracker::BeforeRunTask,执行完成后会经过TaskTracker::AfterRunTask,在这两个过程中都会对阻塞类型任务的数量与是否已经开始退出流程进行判断,如果判断成立则会唤醒原来的退出流程继续执行,以AfterRunTask为例:
void TaskTracker::AfterRunTask(TaskShutdownBehavior shutdown_behavior) {
if (shutdown_behavior == TaskShutdownBehavior::BLOCK_SHUTDOWN ||
shutdown_behavior == TaskShutdownBehavior::SKIP_ON_SHUTDOWN) {
const bool shutdown_started_and_no_tasks_block_shutdown =
state_->DecrementNumTasksBlockingShutdown();
if (shutdown_started_and_no_tasks_block_shutdown)
OnBlockingShutdownTasksComplete();
}
}
void TaskTracker::OnBlockingShutdownTasksComplete() {
AutoSchedulerLock auto_lock(shutdown_lock_);
// This method can only be called after shutdown has started.
DCHECK(state_->HasShutdownStarted());
DCHECK(shutdown_event_);
shutdown_event_->Signal();
}