多线程设计和实现

站在老罗的肩膀上:https://blog.csdn.net/Luoshengyang/article/details/46855395

多线程模型则是为了解决网页的卡顿问题。为了达到这个目的,Chromium的多线程模型是基于异步通信的。

一个典型的异步通信过程如图1所示:

图1  线程异步通信过程

       Task-1被分解成三个子任务Task-1(1)、Task-1(2)和Task-1(3)。其中,Task-1(1)由Thread-1执行。Task-1(1)执行完成后,Thread-1通过Closure请求Thread-2执行Task-1(2)。Task-1(2)执行完成后,Thread-2又通过一个Closure请求Thread-1执行Task-1(3)。至此,Task-1就执行完成。我们可以将第一个Closure看作是一个Request操作,而第二个Closure是一个Reply操作。这是一个典型的异步通信过程。当然,如果不需要知道异步通信结果,那么第二个Closure和Task-1(3)就是不需要的。

假设Thread-1需要知道异步通信的结果,那么在图1中我们可以看到一个非常关键的点:Thread-1并不是什么也不干就只是等着Thread-2执行完成Task-1(2),它趁着这个等待的空隙,干了另外一件事情——Task-2。如果我们将Thread-1看作是一个UI线程,那么就意味着这种异步通信模式是可以提高它的响应性的。

       为了能够完成上述的异步通信过程,一个线程的生命周期如图2所示:

图2 线程生命周期

       线程经过短暂的启动之后(Start),就围绕着一个任务队列(TaskQueue)不断地进行循环,直到被通知停止为止(Stop)。在围绕任务队列循环期间,它会不断地检查任务队列是否为空。如果不为空,那么就会将里面的任务(Task)取出来,并且进行处理。这样,一个线程如果要请求另外一个线程执行某一个操作,那么只需要将该操作封装成一个任务,并且发送到目标线程的任务队列去即可。

为了更好地理解这种基于任务队列的线程运行模式,我们脑补一下另外一种常用的基于锁的线程运行模式。一个线程要执行某一个操作的时候,就直接调用一个代表该操作的一个函数。如果该函数需要访问全局数据或者共享数据,那么就需要进行加锁,避免其它线程也正在访问这些全局数据或者共享数据。这样做的一个好处是我们只需要关心问题的建模,而不需要关心问题是由谁来执行的,只要保证逻辑正确并且数据完整即可。当然坏处也是显然的。首先是为了保持数据完整性,也就是避免访问数据时出现竞争条件,代码里面充斥着各种锁。其次,如果多个线程同时获取同一个锁,那么就会产生竞争。这种锁竞争会带来额外的开销,从而降低线程的响应性。

       基于任务队列的线程运行模式,要求在对问题进行建模时,要提前知道谁是执行者。也就是说,在对问题进行建模时,需要指派好每一个子问题的执行者。这样我们为子问题设计数据结构时,就规定这些数据结构仅仅会被子问题的执行者访问。这样执行者在解决指派给它的问题时,就不需要进行加锁操作,因为在解决问题过程中需要访问的数据不会同时被其它执行者访问。这就是通过任务队列来实现异步通信的多线程模型的设计哲学。

       当然,这并不是说,基于任务队列的线程运行模式可以完全避免使用锁,因为任务队列本身就是一个线程间的共享资源。想象一下,一个线程要往里面添加任务,另一个线程要从里面将任务提取出来处理。因此,所有涉及到任务队列访问的地方都是需要加锁的。但是如果我们再仔细想想,那么就会发现,任务队列只是一个基础设施,它与具体的问题是无关的。因此,只要我们遵循上述设计哲学,就可以将代码里面需要加锁的地方仅限于访问任务队列的地方,从而就可以减少锁竞争带来的额外的开销。

       这样说来,似乎基于任务队列的线程运行模式很好,但是实际上它对问题建模提出了更高的要求,也就是进行子问题划分时,要求划分出来的子问题是正交的,这样我们才有可能为这些子问题设计出不会同时被访问的数据结构。看到“正交”两个字,是不是想起高数里面的向量空间的正交基了?或者傅里叶变换用到的一组三角函数了?其实道理就是一样一样的。

分析Chromium多线程模型的设计和实现,也就是基于任务队列的线程运行模式涉及到核心类图,如图3所示:

图3 基于任务队列的线程运行模式核心类关系图

Thread是一个用来创建带消息循环的类。当我们创建一个Thread对象后,调用它的成员函数Start或者StartWithOptions就可以启动一个带消息循环的线程。其中,成员函数StartWithOptions可以指定线程创建参数。当我们不需要这个线程时,就可以调用之前创建的Thread对象的成员函数Stop。

       Thread类继承了PlatformThread::Delegate类,并且重写了它的成员函数ThreadMain。我们知道,Chromium是跨平台的,这样各个平台创建线程使用的API有可能是不一样的。不过,我们可以通过PlatformThread::Delegate类为各个平台创建的线程提供一个入口点。这个入口点就是PlatformThread::Delegate类的成员函数ThreadMain。由于Thread类重写了父类PlatformThread::Delegate的成员函数ThreadMain,因此无论是哪一个平台,当它创建完成一个线程后,都会以Thread类的成员函数ThreadMain作为线程的入口点。

       Thread类有一个重要的成员变量message_loop_,它指向的是一个MessageLoop对象。这个MessageLoop对象就是用来描述线程的消息循环的。MessageLoop类内部通过成员变量run_loop_指向的一个RunLoop对象和成员变量pump_指向的一个MessagePump对象来描述一个线程的消息循环。

       一个线程在运行的过程中,可以有若干个消息循环,也就是一个消息循环可以运行在另外一个消息循环里面。除了最外层的消息循环,其余的消息的消息循环称为嵌套消息循环。我们为什么需要嵌套消息循环呢?这主要是跟模式对话框有关。

       考虑一个情景,我们在一个窗口弹出一个文件选择对话框。窗口必须要等到用户在文件选择对话框选择了文件之后,才能去做其它事情。窗口是在消息循环过程中打开文件对话框的,它要等待用户在文件选择对话框中选择文件 ,就意味着消息循环被中止了(why???)。由于文件选择对话框也是通过消息循环来响应用户输入的,因此如果打开它的窗口中止了消息循环,就会导致它无法响应用户输入。为了解决这个问题,就要求打开文件选择的窗口不能中止消息循环。方法就是该窗口创建一个子消息循环,该子消息循环负责处理文件选择对应框的输入事件,直到用户选择了一个文件为止。

       MessageLoop类的成员变量run_loop_指向的一个RunLoop对象就是用来记录线程当使用的消息循环的。RunLoop类有三个重要的成员变量:

       1. message_loop_,记录一个RunLoop对象关联的MessageLoop对象。

       2. previous_loop_,记录前一个消息循环,当就是包含当前消息循环的消息循环。

       3. run_depth_,记录消息循环的嵌套深度。

       MessageLoop类的成员变量pump_指向的一个MessagePump对象是用来进行消息循环的,也就是说,Thread类描述的线程通过MessagePump类进入到消息循环中去。

       Thread类将消息划分为三类,分别通过以下三个成员变量来描述:

       1. work_queue_,指向一个TaskQueue对象,用来保存那些需要马上处理的消息。

       2. delayed_work_queue_,指向一个DelayedTaskQueue,用来保存那些需要延迟一段时间再处理的消息。

       3. deferred_non_nestable_work_queue_,指向一个TaskQueue对象,用来保存那些不能够在嵌套消息循环中处理的消息。

       一个MessagePump对象在进行消息循环时,如果发现消息队列中有消息,那么就需要通知关联的MessageLoop对象进行处理。通知使用的接口就通过MessagePump::Delegate类来描述。

       MessagePump::Delegate类定义了四个成员函数,如下所示:

       1. DoWork,用来通知MessageLoop类处理其成员变量work_queue_保存的消息。

       2. DoDelayedWork,用来通知MessageLoop类处理其成员变量delayed_work_queue_保存的消息。

       3. DoIdleWork,用来通知MessageLoop类当前无消息需要处理,MessageLoop类可以利用该间隙做一些Idle Work。

       4. GetQueueingInformation,用来获取MessageLoop类内部维护的消息队列的信息,例如消息队列的大小,以及下一个延迟消息的处理时间。

       有了前面的基础知识,接下来我们就可以大概描述Thread类描述的线程的执行过程。

       首先是线程的启动过程:

       1. 调用Thread类的成员函数Start或者StartWithOptions启动一个线程,并且以Thread类的成员函数ThreadMain作为入口点。

       2. Thread类的成员函数ThreadMain负责创建消息循环,也就是通过MessageLoop类创建消息循环。

       3. MessageLoop类在创建消息循环的过程中,会通过成员函数Init创建用来一个用来消息循环的MessagePump对象。

       4. 消息循环创建完成之后,调用MessageLoop类的成员函数Run进入消息循环。

       5. MessageLoop类的成员函数Run创建一个RunLoop对象,并且调用它的成员函数Run进入消息循环。注意,该RunLoop对象在创建的过程,会关联上当前线程使用的消息循环,也就是创建它的MessageLoop对象。

       6. RunLoop类的成员函数Run负责建立好消息循环的嵌套关系,也就是设置好它的成员变量previous_loop_和run_depth_等,然后就会调用其关联的MessageLoop对象的成员函数RunHandler进入消息循环。

       7. MessageLoop类的成员函数RunHandler调用成员变量pump_描述的一个MessagePump对象的成员函数Run进入消息循环。

       接下来是向线程的消息队列发送消息的过程。这是通过MessageLoop类的以下四个成员函数向消息队列发送消息的:

       1. PostTask,发送需要马上进行处理的并且可以在嵌套消息循环中处理的消息。

       2. PostDelayedTask,发送需要延迟处理的并且可以在嵌套消息循环中处理的消息。

       3. PostNonNestableTask,发送需要马上进行处理的并且不可以在嵌套消息循环中处理的消息。

       4. PostNonNestableDelayedTask,发送需要延迟处理的并且不可以在嵌套消息循环中处理的消息。

       向线程的消息队列发送了新的消息之后,需要唤醒线程,这是通过调用MessagePump类的成员函数Schedule进行的。线程被唤醒之后 ,就会分别调用MessageLoop类重写父类MessagePump::Delegate的两个成员函数DoWork和DoDelayedWork对消息队列的消息进行处理。如果没有消息可以处理,就调用MessageLoop类重写父类MessagePump::Delegate的成员函数DoIdleWork通知线程进入Idle状态,这时候线程就可以做一些Idle Work。

       MessageLoop类的成员函数DoWork在处理消息的过程中,按照以下三个类别进行处理:

       1. 对于可以马上处理的消息,即保存在成员变量work_queue_描述的消息队列的消息,执行它们的成员函数Run。

       2. 对于需要延迟处理的消息,将它们保存在成员变量delayed_work_queue_描述的消息队列中,并且调用成员变量pump_指向的一个MessagePump对象的成员函数ScheduleDelayedWork设置最早一个需要处理的延迟消息的处理时间,以便该MessagePump对象可以优化消息循环逻辑。

       3. 对于可以马上处理但是不可以在嵌套消息循环中处理的消息,如果线程是处理嵌套消息循环中,那么将它们保存在成员变量deferred_non_nestable_work_queue_描述的消息队列中,这些消息将会在线程进入Idle状态时,并且是处理最外层消息循环时,得到处理。

       以上就是Thread类描述的线程的大概执行过程,接下来我们通过源码分析详细描述这些过程。

       我们首先看线程的启动过程,即Thread类的成员函数Start的实现,如下所示:

bool Thread::Start() {
  ...
  return StartWithOptions(options);
}


bool Thread::StartWithOptions(const Options& options) {
  ...

  SetThreadWasQuitProperly(false);

  MessageLoop::Type type = options.message_loop_type;
  if (!options.message_pump_factory.is_null())
    type = MessageLoop::TYPE_CUSTOM;

  message_loop_timer_slack_ = options.timer_slack;
  // create MessageLoop
  std::unique_ptr<MessageLoop> message_loop_owned =
      MessageLoop::CreateUnbound(type, options.message_pump_factory);
  message_loop_ = message_loop_owned.get();
  start_event_.Reset();

  if (options.on_sequence_manager_created) {
    sequence_manager_ =
        sequence_manager::CreateUnboundSequenceManager(message_loop_);
    options.on_sequence_manager_created.Run(sequence_manager_.get());
  }

  // Hold |thread_lock_| while starting the new thread to synchronize with
  // Stop() while it's not guaranteed to be sequenced (until crbug/629139 is
  // fixed).
  {
    AutoLock lock(thread_lock_);
    bool success =
        options.joinable
            ? PlatformThread::CreateWithPriority(options.stack_size, this,
                                                 &thread_, options.priority)
            : PlatformThread::CreateNonJoinableWithPriority(
    ...
  }

  joinable_ = options.joinable;
  ...
}

其中 创建MessageLoop

std::unique_ptr<MessageLoop> message_loop_owned =
     MessageLoop::CreateUnbound(type, options.message_pump_factory);

将当前创建的MessageLoop对象保存在全局变量lazy_tls_ptr指向一块线程局部存储中,这样我们就可以通过MessageLoop类的静态成员函数current获得当前线程的消息循环,如下所示:

// static
void MessageLoopCurrent::BindToCurrentThreadInternal(MessageLoop* current) {
  GetTLSMessageLoop()->Set(current);
}

​base::ThreadLocalPointer<MessageLoop>* GetTLSMessageLoop() {
  static NoDestructor<ThreadLocalPointer<MessageLoop>> lazy_tls_ptr;
  return lazy_tls_ptr.get();
}

 


没找到Init 和 创建task queue 


bool PlatformThread::CreateWithPriority(size_t stack_size, Delegate* delegate,
                                        PlatformThreadHandle* thread_handle,
                                        ThreadPriority priority) {
  return CreateThread(stack_size, true /* joinable thread */, delegate,
                      thread_handle, priority);
}


bool CreateThread(size_t stack_size,
                  bool joinable,
                  PlatformThread::Delegate* delegate,
                  PlatformThreadHandle* thread_handle,
                  ThreadPriority priority) {
  base::InitThreading();
  ...
 
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值