Chromium 的多线程机制

优质
小牛编辑
130浏览
2023-12-01

前面我们介绍过Chromium是基于多进程模型的架构设计,那么各个进程内的情况呢?事实是每个进程都有很多的线程,特别是browser进程,因而它也基于多线程模型的。介绍多线程机制之前,先来看一下残酷的现实吧,下面是各个进程的线程信息情况(基于Linux平台,其它平台的可能略有不同),相信保证让你头大。是的,你需要泡杯茶,然后静下心来了解一下它们: enter image description here

为什么这么多的线程呢?Chromium的官方说法告诉我们,主要目的就是为了保持UI的高响应度,保证UI线程(chrome线程,主线程)不会被任何其它费时的操作阻碍从而影响了对用户的响应。这些其它的操作很多,例如本地文件读写,socket读写,数据库操作等等。既然它们会阻碍其它操作,那好,把它们放在单独的线程里自己忙或者等待去吧,所以你就看到那么多与这些相关的线程(线程名显然也暴露了这一切)。

问题来了,它们之间如何通信和同步呢?这是多线程的一个非常难缠的问题,因为这会造成死锁或者竞争冲突等问题。Chromium精心设计了一套机制来处理它们,那就是绝大多数的场景使用事件和一种chromium新创建的任务传递机制,仅在非用不可的情况下使用锁或者线程安全对象,这有严格的要求,详细的情况请查看以下链接以便作近一步的了解: http://www.chromium.org/developers/lock-and-condition-variable。

问题又来了,那么每个线程内部是如何处理这些事件和任务的呢?答案是MessageLoop。每个线程会有一个自己的MessageLoop,它们用来处理这些事件和任务。通常的MessageLoop只是处理事件,Chromium中的MessageLoop可以同时处理事件和任务。MessageLoop也是值得研究的,详细情况我们将在以后的一章加以介绍。

任务和MessageLoop的基本原理如下图所示。任务被派发到进程的某个线程的MessageLoop的队列中,MessageLoop会调度执行这些Task。

enter image description here

关于上面这些线程,多数可以通过它们的名字猜出用途,这里鉴于篇幅和噪音考虑,不一一介绍,下面说明几个重要和诡异的线程:

chrome线程:进程的主线程,browser进程重要主要是负责UI,当然也是管家;Renderer进程中则是管家兼处理WebKit渲染的;gpu进程中则是负责处理处理绘图请求并调用openGL进行绘制工作的。

Chrome_IOThread/Chrome_ChildIOThread线程:用来接受来自其它进程的IPC消息和派发自身消息到其它进程。

SignalSender线程: V8 JavaScript引擎中用于处理Linux信号的线程。

任务(task)

Chromium的特色就是在事件的基础上,加入了一个新的机制-任务。当需要执行某个操作时候,可以把该操作封装成一个任务,由任务派发机制传递给相应的进程的MessageLoop。 下面看看线程内和线程间分别是如何操作的。

首先看线程内部是如何进行的。但你需要进行费时的操作时候,可以派发一个事件和回调函数给自身线程的MessageLoop,然后MessageLoop会调度该回调函数以执行其操作。问题是这有必要吗?直接调用不就可以吗?答案是不可以,或者说是最好不要这么做,其原因在于,如果当前的MessageLoop里面有优先级更高的事件和任务需要处理时,你这样做会阻碍它们的执行。 其次看一看线程间通信。假如一个线程A需要把任务传递给一个另外的线程B,大致有三个阶段:

  1. 首先,线程A把该任务传递给线程B;
  2. 其次,线程B调度执行该任务;
  3. 最后,线程B执行完任务后回复A。很多情况下,线程A不需要回复。 下图描述的是一个带回复的典型的任务传递过程。

enter image description here

如果不需要回复,那么上图中的‘Reply Task2’就不需要了。当需要回复时候,chromium的做法是新建一个新的任务,该任务封装原来的任务,新任务被放入线程B,B执行新任务时候调用其Run方法,里面首先执行原来的任务,然后派发Reply任务给线程A,操作完成。 后面会有一个具体的例子来描述上面这个过程,下面了解一下chromium支持Task所涉及的几个主要类。 Callback: 回调类,其本质是封装了一个由调用者(例如线程A)设置的回调函数。包含一个run方法,当MessageLoop调度执行该Task时候,运行该方法,该方法调用回调函数 Closure: 定义为Callback<void(void)> tracked_objects:一系列的类用于追踪Task生成位置,编译调试 Task: 包含一个Closure,追踪信息,表示一个任务

一个例子

下面用一个实际的例子来理解线程间是如何传递任务的。该例子是chromium中非常常见的一个场景: browser进程接收到来自renderer进程的IPC消息,它由IO线程接收管理,然后派发给chrome线程处理,具体过程如下图所示:

enter image description here

上面的图基本上对应了前面的关于任务派发过程的图,只是少了回复环节,这是因为IO 线程并不需要回复。基本的步骤是,当IO线程收到消息后,其派发一个任务,该任务将ChannelProxy::Context::OnDispatchMessage设置会回调函数,这个任务保存在chrome线程的输入任务队列中。chrome线程将输入队列拷贝到工作队列后,执行该任务的run方法,该方法会调用回调函数ChannelProxy::Context::OnDispatchMessage,该函数会调用事件的处理函数,这里也就是RenderProcessHostImpl::OnMessageReceived,这个函数实际上会根据事件类型来调用各个相应函数。

源文件目录

base/threading/
         线程相关的基础类

参考文献

  1. http://www.chromium.org/developers/design-documents/threading
  2. http://bigasp.com/archives/478
  3. http://www.chromium.org/developers/lock-and-condition-variable