原文地址 - Chromium Docs - Threading and Tasks in Chrome (googlesource.com) 最后编辑时间 - 2022/7/24 机翻后经过校对 / MIT License
Note: See Threading and Tasks FAQ for more examples.
概述
Chrome 是多进程架构(Multi-process Architecture)的,每个进程都持有大量的线程。在本文档中,我们将介绍基本线程系统,该系统在所有进程中都是通用的。系统的主要目标是保障主线程(也就是浏览器进程中的 "UI "线程)和 IO 线程(每个进程用于接收 IPC 的线程)的响应速度。这意味着这两个线程会将任何导致阻塞的 I/O 操作或其他高开销操作交付给其他线程执行。系统线程间通信的方式是消息传递。我们不鼓励使用带锁(lock)和线程安全(thread-safe)的对象。相反,对象只在一个(通常是虚拟的--我们稍后会提到!)线程上运行,而我们在这些线程之间传递消息进行通信。如果没有关于延迟或工作量的外部要求,Chrome 应该是一个高并发的系统,但不一定是并行的(并发与并行有什么区别?)。
关于Chromium的并发方式(尤其是序列(Sequences))的基本介绍可以在这里找到。
该文档假设你已经熟悉了计算机科学的线程概念。
核心概念
- 任务(Task):一个待执行工作的单元。实际上是一个函数指针,可以选择任务相关的状态。在 Chrome 中,任务是由
base::BindOnce和base::BindRepeating分别创建的base::OnceCallback和base::RepeatingCallback对象(相关文档)。 - 任务队列(Task queue):一个待执行任务的队列。
- 物理线程(Physical Thread):由操作系统提供的线程(例如,POSIX 上的 pthread 或 Windows 上的 CreateThread())。你几乎不应该直接使用物理线程。
base::Thread:一个直到 Quit() 之前都会不停地处理来自特定任务队列的消息的物理线程。你几乎不应该自己创建base::Thread。- 线程池(Thread pool):一个拥有共享任务队列的物理线程池。在 Chrome 中,其实现为
base::ThreadPoolInstance。每个 Chrome 进程有且只有一个线程池实例,它为通过base/task/thread_pool.h发布的任务提供服务,因此你应该很少需要直接使用base::ThreadPoolInstance的 API(后面有关于发布任务的更多内容)。 - 序列(Sequence) 或 虚拟线程(Virtual thread):一个由 chrome 管理的执行线程。像物理线程一样,在任何特定的时刻,只有一个任务可以在一个给定的序列/虚拟线程上运行,每个任务都能看到前面的任务的副作用(side-effects)。任务是按顺序执行的,但在每个任务可能执行在不同的物理线程上。
- 任务运行器(Task runner):一个可以发布任务的接口。在Chrome中的实现是
base::TaskRunner。 - 序列任务运行器(Sequenced task runner):一个任务运行器,它保证通过它发布的任务将按照发布的顺序依次运行。每个这样的任务都能保证看到它前面的任务的副作用。发布到序列任务运行器的任务通常由一个线程(虚拟或物理)处理。在 Chrome 中的实现是
base::SequencedTaskRunner,它继承了base::TaskRunner。 - 单线程任务运行器(Single-thread task runner):一个序列任务运行器,它保证所有的任务都由同一个物理线程来处理。在 Chrome 中的实现是
base::SingleThreadTaskRunner,它继承额base::SequencedTaskRunner。只要有可能,我们更倾向于使用序列而不是线程。
线程词库
读者须知:以下术语是为了弥补普通线程术语和我们在 Chrome 中的使用方式之间的差距。如果你是个初学者,可能会有点困难。如果对你来说这些东西很难理解,你可以考虑跳到下面更详细的部分,必要时再参考一下。
- 线程不安全(Thread-unsafe):Chrome 浏览器中的绝大多数类型都是线程不安全的(故意如此)。对这类类型 / 方法的访问必须在外部进行同步。典型的线程不安全类型要求所有访问其状态的任务都被发布到同一个
base::SequencedTaskRunner上,并且在调试构建(debug builds)中用SEQUENCE_CHECKER成员来验证。锁也是同步访问的一种选择,但在 Chrome 浏览器中,我们更喜欢使用序列而非锁。 - 线程亲和(Thread-affine):这样的类型/方法需要总是通过同一个物理线程(即从同一个
base::SingleThreadTaskRunner)访问,通常由一个 THREAD_CHECKER 成员来验证这一点。除了使用第三方 API 或者叶子依赖(译者注:依赖树的叶节点)有线程亲和特性:在 Chrome 中,几乎没有理由让一个类型为线程亲和的。请注意,base::SingleThreadTaskRunner继承了base::SequencedTaskRunner,所以线程亲和是线程不安全的一个子集。线程亲和有时也被称为线程对立(thread-hostile)。 - 线程安全(Thread-safe):这样的类型/方法可以安全地进行并行访问。
- 线程兼容(Thread-compatible):这种类型提供了对常量方法(const methods)的安全并行访问,但对于非常量(或常量/非常量混合访问)则需要同步。Chrome 不提供读写锁;因此,唯一的用例是对象(通常是全局对象)以线程安全的方式初始化一次(要么在启动的单线程阶段,要么通过线程安全的静态-本地初始化范式(如
base::NoDestructor)进行懒初始化(译者注:在第一次访问时才初始化)),之后永远不可变。 - 不可变的(Immutable):线程兼容类型的一个子集,在构建后不能被修改。
- 序列友好(Sequence-friendly):这种类型/方法是线程不安全的类型,支持在
base::SequencedTaskRunner中调用。理想情况下,所有的线程不安全类型都是如此,但祖传代码有时会有过度的检查,在单纯的线程不安全情况下强制要求线程亲和性。更多细节请参见下面的选择序列而非物理线程。
线程(Thread)
每个 Chrome 进程都有:
- 一个主线程(main thread)
- 在浏览器进程(BrowserThread::UI)中负责更新 UI
- 在渲染进程(Blink 的主线程)中负责运行大多数的 Blink (译者注:Blink 是 Chromium 的渲染引擎)
- 一个 IO 线程(IO thread)
- 在所有进程中,所有的 IPC 消息都发送给这个线程,但处理消息的应用逻辑可能在不同的线程中(即,IO 线程可能将消息路由到绑定到不同线程的 Mojo 接口(Mojo interface))。
- 更普遍的是,大多数异步 I/O 发生在这个线程上(例如,通过
base::FileDescriptorWatcher)。 - 在浏览器进程中,这被称为BrowserThread::IO。
- 还有一些特殊用途的线程
- 和一个通用线程池
大多数线程都有一个循环,不停地从队列中获取任务并将之运行(该队列可能在多个线程之间共享)。
任务(Task)
一个任务是一个被添加到一个队列中用于异步执行的 base::OnceClosure。
一个 base::OnceClosure 存储一个函数指针和参数。它有一个 Run() 方法,使用绑定的参数调用函数指针。它是用 base::BindOnce 创建的。(参考 Callback<> 和 Bind() 文档)。
void TaskA() {}
void TaskB(int v) {}
auto task_a = base::BindOnce(&TaskA);
auto task_b = base::BindOnce(&TaskB, 42);
一组任务可以通过以下方式之一来执行:
- 并行(Parallel):没有任务执行顺序,可能在任何时候在任何线程上执行。
- 序列(Sequenced):任务按发布顺序执行,可能在任何线程上执行,同一时间只有一个任务在执行。
- 单线程(Single Threaded):任务按发布顺序执行,在单个线程上一次执行一个。
- COM 单线程(COM Signgle Threaded):单线程的一个变种,初始化了 COM。
选择序列而非物理线程
序列执行(于虚拟线程上)比单线程执行(于物理线程上)更有优势。除了与主线程(UI)或 IO 线程绑定的类型/方法:通过 base::SequencedTaskRunner 实现线程安全比通过自己管理物理线程更好(参考下面的发布序列任务)。
所有为 "当前物理线程 "暴露的 API 都有一个对应的 "当前序列"版本(如何从单线程迁移到序列?))。
如果你发现自己编写的序列友好型类型在叶子依赖中未能通过线程亲和检查(例如 THREAD_CHECKER):考虑将该依赖也变成序列友好型。Chrome 浏览器中的大多数核心 API 都是序列友好型的,但一些传统类型仍然可能过度热衷于使用 ThreadChecker/ThreadTaskRunnerHandle/SingleThreadTaskRunner,而它们可以依靠 "当前序列 "而不再是线程亲和去实现。
发布并行任务
直接发布到线程池
一个可以在任何线程上运行,并且与其他任务没有顺序或互斥要求的任务,应该使用一个 base::ThreadPool::PostTask*() 中的函数,这些函数定义在 base/task/thread_pool.h。
base::ThreadPool::PostTask(FROM_HERE, base::BindOnce(&Task));
这将使用默认的 traits 发布任务。
base::ThreadPool::PostTask*() 函数允许调用者通过 TaskTraits 提供关于任务的额外细节(参考:用TaskTraits 注释任务)。
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, MayBlock()},
base::BindOnce(&Task));
通过任务运行器发布
使用并行的 base::TaskRunner 是直接调用 base::ThreadPool::PostTask*() 的一个替代方案。这主要是在事先不知道任务是以并行、序列还是单线程方式发布的情况下有用(参考:发布序列任务,发布多个任务到同一线程)。由于 base::TaskRunner 是 base::SequencedTaskRunner 和 base::SingleThreadTaskRunner 的基类,一个 scoped_refptr<TaskRunner> 成员可以持有一个 base::TaskRunner、一个base::SequencedTaskRunner 或者一个 base::SingleThreadTaskRunner。
class A {
public:
A() = default;
void PostSomething() {
task_runner_->PostTask(FROM_HERE, base::BindOnce(&A, &DoSomething));
}
void DoSomething() {
}
private:
scoped_refptr<base::TaskRunner> task_runner_ =
base::ThreadPool::CreateTaskRunner({base::TaskPriority::USER_VISIBLE});
};
除非测试需要精确控制任务的执行方式,否则最好直接调用 base::ThreadPool::PostTask*()(参考测试章节中在测试过程里以较低的侵入性控制任务的方法)。
发布序列任务
一个序列是一组按发布顺序运行的任务,同一时间只有一个序列中的任务在运行(不一定在同一个线程上)。要将在序列上发布任务,请使用 base::SequencedTaskRunner。
发布到一个新序列
一个 base::SequencedTaskRunner 可以通过 base::ThreadPool::CreateSequencedTaskRunner() 创建。
scoped_refptr<SequencedTaskRunner> sequenced_task_runner =
base::ThreadPool::CreateSequencedTaskRunner(...);
// TaskB runs after TaskA completes.
sequenced_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskA));
sequenced_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskB));
发布到当前(虚拟)线程
发布到当前(虚拟)线程的首选方式是通过 base::SequencedTaskRunnerHandle::Get()。
// The task will run on the current (virtual) thread's default task queue.
base::SequencedTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&Task);
注意,SequencedTaskRunnerHandle::Get() 返回当前虚拟线程的默认任务队列。在有多个任务队列的线程上(例如 BrowserThread::UI),你得到的可能是一个不同于当前任务所属队列的队列。"当前" 任务运行器被设计为不通过静态获取器来暴露给外部。因此要么你已经知道哪个运行器是当前运行器并可以直接发布到它,要么你不知道,这时唯一合理的目的地是默认队列。
使用序列而非锁
在 Chrome 中不鼓励使用锁。序列本身就提供了线程安全。宁愿选择总是从同一序列访问的类,也不要用锁来自己管理线程安全性。
线程安全,但不是线程亲和;这是如何实现的? 发布到同一序列的任务将按顺序运行。在一个序列任务完成后,下一个任务可能会被不同的工作线程接走,但该任务保证会看到前一个(几个)任务对其序列造成的任何副作用。
class A {
public:
A() {
// Do not require accesses to be on the creation sequence.
DETACH_FROM_SEQUENCE(sequence_checker_);
}
void AddValue(int v) {
// Check that all accesses are on the same sequence.
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
values_.push_back(v);
}
private:
SEQUENCE_CHECKER(sequence_checker_);
// No lock required, because all accesses are on the
// same sequence.
std::vector<int> values_;
};
A a;
scoped_refptr<SequencedTaskRunner> task_runner_for_a = ...;
task_runner_for_a->PostTask(FROM_HERE,
base::BindOnce(&A::AddValue, base::Unretained(&a), 42));
task_runner_for_a->PostTask(FROM_HERE,
base::BindOnce(&A::AddValue, base::Unretained(&a), 27));
// Access from a different sequence causes a DCHECK failure.
scoped_refptr<SequencedTaskRunner> other_task_runner = ...;
other_task_runner->PostTask(FROM_HERE,
base::BindOnce(&A::AddValue, base::Unretained(&a), 1));
锁应该只用于在不同线程间交换一个可以在多个线程上访问的共享数据结构的所有权。如果一个线程的更新需要的计算开销很高或必须访问磁盘访问,那么这个缓慢的工作应该在不持有锁的情况下完成。只有当结果可用时,才应该使用锁来交换更新后的数据。PluginList::LoadPlugins(content/browser/plugin_list.cc)就是这样一个例子。如果你必须使用锁,这里有一些最佳实践和要避免的陷阱。
为了编写非阻塞代码,Chrome 中的许多 API 都是异步的。通常这意味着它们要么需要在特定的线程/序列上执行,并将通过自定义的委托(delegate)接口返回结果,要么就是获取一个 base::OnceCallback<>(或base::RepeatingCallback<>)对象,在请求的操作完成后被调用。在一个特定的线程/序列上执行工作相关的内容,在上面的 PostTask 部分有所涉及。
发布多个任务到同一线程
如果多个任务需要在同一个线程上运行,请将它们发布到一个 base::SingleThreadTaskRunner。所有发布到同一个 base::SingleThreadTaskRunner 的任务都会按照发布的顺序在同一个线程上运行。
发布到浏览器进程的主线程或 IO 线程
要向主线程或 IO 线程发布任务,请使用 content::GetUIThreadTaskRunner({}) 或 content::GetIOThreadTaskRunner({}),他们声明于 content/public/browser/browser_thread.h
你可以提供额外的 BrowserTaskTraits 作为这些方法的参数,尽管这在 BrowserThreads 中一般还是不常见的,这种用法是给高级用例提供的。
目前正在进行迁移(task API v3),不再使用以前的 base-API-with-traits,但你仍然可以在整个代码库中找到它(它们是等价的):
base::PostTask(FROM_HERE, {content::BrowserThread::UI}, ...);
base::CreateSingleThreadTaskRunner({content::BrowserThread::IO})
->PostTask(FROM_HERE, ...);
注意:在迁移期间,你将不幸地需要继续手动 including content/public/browser/browser_task_traits.h 以使用 browser_thread.h API。
主线程和 IO 线程已经超级繁忙了。因此,在可能的情况下,我们更倾向于将任务发布到通用线程(参考:发布并行任务、发布序列任务)。发布任务到主线程的好理由是为了更新用户界面或访问与之绑定的对象(例如,Profile)。发布任务到 IO 线程的好理由是访问与其绑定的组件的内部(如 IPC,网络)。注意:如果只是为了发送/接收 IPC 或在网络上发送/接收数据,没有必要向 IO 线程显式地发布任务。
发布到渲染进程的主线程
TODO(blink-dev)
译者注:官方就是 TODO,不是我偷懒……
发布到自定义的单线程任务运行器
如果多个任务需要在同一个线程上运行,而且这个线程不一定是主线程或 IO 线程,可以将它们发布到由 base::Threadpool::CreateSingleThreadTaskRunner 创建的一个 base::SingleThreadTaskRunner。
scoped_refptr<SingleThreadTaskRunner> single_thread_task_runner =
base::Threadpool::CreateSingleThreadTaskRunner(...);
// TaskB runs after TaskA completes. Both tasks run on the same thread.
single_thread_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskA));
single_thread_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskB));
请记住,我们更喜欢选择序列而非物理线程,因此应该很少需要这样做。
发布到当前线程
重要提示: 要发布一个需要与当前任务序列互斥但又不绝对需要在当前物理线程上运行的任务,请使用
base::SequencedTaskRunnerHandle::Get()而不是base::ThreadTaskRunnerHandle::Get()(参考:发布到当前序列)。这将更好地记录发布任务的要求,并将避免不必要地使你的 API 物理线程化。在一个单线程任务中,base::SequencedTaskRunnerHandle::Get()等同于base::ThreadTaskRunnerHandle::Get()。
如果你无论如何都要向当前的物理线程发布任务,使用 base::ThreadTaskRunnerHandle。
// The task will run on the current thread in the future.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&Task));
发布到 COM 单线程容器(Single-Thread Apartment, STA)线程(Windows)
需要在 COM STA 线程上运行的任务必须被发布到由 base::ThreadPool::CreateCOMSTATaskRunner() 返回的 base::SingleThreadTaskRunner。正如在将多个任务发布到同一个线程中所提到的,所有发布到同一个 base::SingleThreadTaskRunner 的任务都会按照发布顺序在同一个线程中运行。
// Task(A|B|C)UsingCOMSTA will run on the same COM STA thread.
void TaskAUsingCOMSTA() {
// [ This runs on a COM STA thread. ]
// Make COM STA calls.
// ...
// Post another task to the current COM STA thread.
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&TaskCUsingCOMSTA));
}
void TaskBUsingCOMSTA() { }
void TaskCUsingCOMSTA() { }
auto com_sta_task_runner = base::ThreadPool::CreateCOMSTATaskRunner(...);
com_sta_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskAUsingCOMSTA));
com_sta_task_runner->PostTask(FROM_HERE, base::BindOnce(&TaskBUsingCOMSTA));
用 TaskTraits 注释任务
base::TaskTraits 封装了关于任务的信息,帮助线程池做出更好的调度决定。
接受 base::TaskTraits 的方法可以在默认 traits 够用的情况下可以设置为 {}。默认 traits 适用于以下任务。
- 非阻塞(参考:MayBlock 和 WithBaseSyncPrimitives)。
- 适用于 user-bloking activity;(显式或隐式地通过组件的排序依赖关系来实现)。
- 可以 block 关机或在关机时被跳过(线程池可以自由选择合适的默认值)。不符合这一描述的任务发布时必须显式地设置 TaskTraits。
base/task/task_traits.h 提供了可用 traits 的详尽文档。内容层还在 content/public/browser/browser_task_traits.h 中提供了额外的 traits,以方便将任务发布到 BrowserThread 上。
下面是一些关于如何指定 base::TaskTraits 的例子。
// This task has no explicit TaskTraits. It cannot block. Its priority is
// USER_BLOCKING. It will either block shutdown or be skipped on shutdown.
base::ThreadPool::PostTask(FROM_HERE, base::BindOnce(...));
// This task has the highest priority. The thread pool will schedule it before
// USER_VISIBLE and BEST_EFFORT tasks.
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::USER_BLOCKING},
base::BindOnce(...));
// This task has the lowest priority and is allowed to block (e.g. it
// can read a file from disk).
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskPriority::BEST_EFFORT, base::MayBlock()},
base::BindOnce(...));
// This task blocks shutdown. The process won't exit before its
// execution is complete.
base::ThreadPool::PostTask(
FROM_HERE, {base::TaskShutdownBehavior::BLOCK_SHUTDOWN},
base::BindOnce(...));
保证浏览器响应速度
不要在主线程、IO线程或任何预期以低延迟运行任务的序列上执行高代价的工作。取而代之的是,使用 base::ThreadPool::PostTaskAndReply*() 或 base::SequencedTaskRunner::PostTaskAndReply() 来异步执行。请注意,IO 线程上的 异步/重叠 IO 是可以的。
示例:在主线程上运行下面的代码会使浏览器长时间无法响应用户输入。
// GetHistoryItemsFromDisk() may block for a long time.
// AddHistoryItemsToOmniboxDropDown() updates the UI and therefore must
// be called on the main thread.
AddHistoryItemsToOmniboxDropdown(GetHistoryItemsFromDisk("keyword"));
下面的代码是一种解决问题的方式,其方式是通过在线程池中调度函数。调度的对象是初始序列(在本例中是 main thread)中紧跟在 AddHistoryItemsToOmniboxDropbox() 之后的 GetHistoryItemsFromDisk()。第一个调用的返回值自动地被提供给第二个调用。
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, {base::MayBlock()},
base::BindOnce(&GetHistoryItemsFromDisk, "keyword"),
base::BindOnce(&AddHistoryItemsToOmniboxDropdown));
延迟发出任务
延迟发出一次性任务
使用 base::ThreadPool::PostDelayedTask*() 或者 base::TaskRunner::PostDelayedTask() 来运行必须在延迟一段时间后才能执行的一次性任务。
base::ThreadPool::PostDelayedTask(
FROM_HERE, {base::TaskPriority::BEST_EFFORT}, base::BindOnce(&Task),
base::Hours(1));
scoped_refptr<base::SequencedTaskRunner> task_runner =
base::ThreadPool::CreateSequencedTaskRunner(
{base::TaskPriority::BEST_EFFORT});
task_runner->PostDelayedTask(
FROM_HERE, base::BindOnce(&Task), base::Hours(1));
注意:一个延迟一个小时才执行的任务可能不需要在延迟结束之后就立刻执行。应该特殊设置一下
base::TaskPriority::BEST_EFFORT来避免这种任务在延迟结束时降低浏览器的运行速度。
延迟发出重复性任务
使用 base::RepeatingTimer 来发布必须定期运行的任务。
class A {
public:
~A() {
// The timer is stopped automatically when it is deleted.
}
void StartDoingStuff() {
timer_.Start(FROM_HERE, Seconds(1),
this, &A::DoStuff);
}
void StopDoingStuff() {
timer_.Stop();
}
private:
void DoStuff() {
// This method is called every second on the sequence that invoked
// StartDoingStuff().
}
base::RepeatingTimer timer_;
};
取消一个任务
使用 bash::WeakPtr
base::WeakPtr 可以用来确保绑定在对象上的 callback 会与对象一起析构。
int Compute() { … }
class A {
public:
void ComputeAndStore() {
// Schedule a call to Compute() in a thread pool followed by
// a call to A::Store() on the current sequence. The call to
// A::Store() is canceled when |weak_ptr_factory_| is destroyed.
// (guarantees that |this| will not be used-after-free).
base::ThreadPool::PostTaskAndReplyWithResult(
FROM_HERE, base::BindOnce(&Compute),
base::BindOnce(&A::Store, weak_ptr_factory_.GetWeakPtr()));
}
private:
void Store(int value) { value_ = value; }
int value_;
base::WeakPtrFactory<A> weak_ptr_factory_{this};
};
注意:WeakPtr 不是线程安全的,~WeakPtrFactory() 和 Store()(与 WeakPtr 绑定)必须全部在同一个序列上运行。
使用 base::CancelableTaskTracker
base::CancelableTaskTracker 允许在任务执行序列的不同序列上发出取消请求。记住 CancelableTaskTracker 不能取消已经开始执行的任务。
auto task_runner = base::ThreadPool::CreateTaskRunner({});
base::CancelableTaskTracker cancelable_task_tracker;
cancelable_task_tracker.PostTask(task_runner.get(), FROM_HERE,
base::DoNothing());
// Cancels Task(), only if it hasn't already started running.
cancelable_task_tracker.TryCancelAll();
发布要并行运行的作业(Job)
base::PostJob 是一个强大的用户 API,它能够调度一个单一的 base::RepeatingCallback worker 任务,然后要求线程池 worker 对它并行调用。这避免了一些造成性能退化的情形:
- 对每个工作单元调用
PostTask(),造成大量开销。 - 固定数量的
PostTask()调用会拆分工作,这可能运行很长时间。当许多组件发布了 “num cores” 任务并且都需要使用所有 core 时,可能会藏成一些问题。在这些情况下,调度程序缺乏上下文来公平对待多个相同优先级的请求和 / 或在高优先级工作到来时请求低优先级工作让出(yield)的能力。
base/task/job_perftest.cc 给出了一个复杂的例子。
// A canonical implementation of |worker_task|.
void WorkerTask(base::JobDelegate* job_delegate) {
while (!job_delegate->ShouldYield()) {
auto work_item = TakeWorkItem(); // Smallest unit of work.
if (!work_item)
return:
ProcessWork(work_item);
}
}
// Returns the latest thread-safe number of incomplete work items.
void NumIncompleteWorkItems(size_t worker_count) {
// NumIncompleteWorkItems() may use |worker_count| if it needs to account for
// local work lists, which is easier than doing its own accounting, keeping in
// mind that the actual number of items may be racily overestimated and thus
// WorkerTask() may be called when there's no available work.
return GlobalQueueSize() + worker_count;
}
base::PostJob(FROM_HERE, {},
base::BindRepeating(&WorkerTask),
base::BindRepeating(&NumIncompleteWorkItems));
通过调用时在循环中尽可能多地执行工作,worker 任务避免了调度开销。同时,定期调用base::JobSecate::ShouldYield() 以有条件地退出并让调度程序优先处理其他工作。举个例子来说明这种让出语义:允许用户可见(user-visible)的作业使用所有内核,但当用户阻塞(user-blocking)任务进来时则让出。