Chromium 中的并发

2024-11-25 pv

Chromium 是一个多进程(Multi-Process)的程序。

进程之间信息传递,通过 IPC (Inter-Process Communication,进程间通信),因为不同进程的内存空间不同。

进程内部,为充分利用多核并提高性能,通常采用多线程(Multi-Thread)的方式。

尽管对于多个线程而言,数据共享是容易的,但安全的数据共享不是,因为有数据竞争(Data Race),不同线程对同一块内存的访问顺序需要协调。

通常采用原子操作,加锁等。

C++11 后,STL 中有诸如 std::atomic<T>std::mutex 这些基础设施。

不过 Chromium 的开发,早在 C++11 前,而且超前的使用了一种新颖的,面向通信的模型,让多线程开发像单线程一样简单、清晰、正确。

让开发者更加关注任务执行的顺序,而非线程调度

1. 经典并发模型

计算机行业发展的原始动力,来自于人们总想更快、更好地完成任务

提高任务的并发度,是一种行之有效的方式。

多核硬件出现,并发编程逐渐流行。

人们发现,它可以有效提高程序的运行效率,尽管会带来额外的复杂度。

并发模型可以理解为一种让并发编程高效且正确工作的机制

经典的有三种:

  1. 共享内存(Share memory)
  2. 通信(Communicate)
  3. 混合(Hybrid)

第一种最常见,也是主流用法。

通过共享内存通信。

多个线程共享同一块内存,内存的改动对于所有线程可见。不过,读写操作之间需要控制顺序,否则便会出现脏读或脏写的问题。

这就是锁、条件变量的用武之处。

第二种,

通过通信共享内存。

这是 Go 语言推崇的 并发模型🔗

线程之间不会同时访问同一块内存。它们之间通过传递讯息(message-passing)完成交流。

好处是,不同的讯息之间,有严格顺序:上一条处理完,下一条才会继续。

不需要锁这种来自外部的协调

第三种模型同时支持上述两种。

2. Chromium 中的模型

Chromium 支持混合模型,但更青睐通信的方式。

所以在 Chromium 仓库中,很少见到锁。

在 Chromium 的作者看来,对于开发者,线程(Thread)是一个粒度很粗,而且比较重的工具,抽象程度低,使用难度大。

其实开发者更在意的,是完成任务(Task),线程只是处理任务的一种手段

而根据任务特点,可归纳为三种可能情况:

  • 独立任务
  • 有顺序依赖的任务
  • 顺序依赖,且必须由同一个线程执行的任务

对于第一种,任务之间独立,任何线程都可以执行,经典的线程池(Thread Pool)可以很好地解决。

第二种需要技巧。比较容易想到,让所有的待执行任务,按照顺序,排列在 FIFO 队列(Queue)中。任务不一定由同一个线程执行,但需要保证,下一个任务必须等待,直到上一个任务完成

第三种,是第二种情况的特化,所有任务只能由一条线程执行。

Chromium 实现了这三种任务的处理类:

  • ThreadPool,线程池
  • SequencedTaskRunner,顺序执行队列
  • SingleThreadTaskRunner,单线程执行队列

3. 启示

锁,条件变量,本质要去控制的,其实是顺序。当以一种隐晦而曲折的方式使用时,易用性就变差了。

队列则将隐含的顺序显像化,直观、清晰,容易验证正确性。

利用经典数据结构,巧妙地解决多线程协调(Coordinate)的问题。

4. 总结

一直认为,计算机科学很务实,就是回答如何更好地解决问题

数据结构,算法,代码都是载体,是工具。

真正有价值的,是我们如何利用它们,解决生活中一个又一个的问题。

(完)

参考

  1. Chromium’s Concurrency model🔗
  2. Threading and Tasks in Chrome🔗
  3. Threading and Tasks in Chrome - FAQ🔗
在 GitHub 上编辑本页面

最后更新于: 2024-11-25T08:05:23+08:00