chromium代码学习

chromium代码学习

chromium开源工程中有太多值得学习的地方,值得我们通过一个单独的专题来记录对这个“伟大”工程的学习

base库Event事件实现方式

对于可等待的通知事件来说,“等待”部分实现的核心就是用锁来进行实现。在POSIX标准下,通过pthread_mutex_t来实现锁的功能。“通知”部分实现的核心则是通过条件变量来实现。在POSIX标准下,通过pthread_cond_t来实现通知的功能,pthread_cond_t需要一个pthread_mutex_t的配合来完成通知的功能。

主要的流程是

pthread_cond_init(&cond) //初始化pthread_cond_t变量
pthread_mutex_lock(&mutex) //锁住pthread_mutex_t变量
pthread_cond_wait(&cond, &mutex) //首先解锁mutex,然后等待cond被signal激活(线程X)
pthread_cond_signal(&cond) //将会signal激活等待在这个条件变量上的线程(并不是调度这个线程执行),并锁住mutex
pthread_mutex_unlock(&mutex) //解锁pthread_mutex_t变量

最后线程X,被OS调度后会执行wait后续的代码。POSIX为什么会前后两次lock、unlock呢?因为可等待的通知事件是可以被用在多个线程中,这些线程可以同时等待某个事件的完成。那么wait函数就需要是线程安全的,但wait函数要把等待线程的信息和事件关联起来,以及事件signal以后把线程信息和事件解除关联。所以就需要lock、unlock两次。

基于base库的APP全流程 基础消息循环

程序的入口:src/ios/wework/main.mm

作为普通的C语言入口函数main,其中实现的最重要的当然是消息循环。消息循环这块后面会专门的开专题来进行讲述。
除此之外,这个入口文件(函数)还负责:

  • [x] A、给版本号全局变量APP_VERSION进行赋值。
  • [x] B、解析命令行参数并将参数存入内存,供后续的初始化使用。需要特别注意的是,wework并没有使用到命令行,所以这个功能是可选的。
  • [x] C、wework的将运行时用到的信息定义在WeWorkRuntime这个类里面,顾名思义就是负责整个app运行时环境所需要的一切的类。这个类肯定很庞杂,故而我们需要一个deleagte类来帮助WeWorkRuntime来完成一些辅助的工作如根据命令行启动一些观察工作、初始化log系统。这个delegate存在的意义,为程序的初始化划分了不同的阶段让我们可以为不同的工作的不同阶段进行一个合理的安排。
  • [x] D、在这个函数内部,会记录程序启动的时间点供后续的各个数据统计上报项使用。

运行时的环境:src/common/wework_runtime.h src/common/wework_runtime.cpp

作为APP运行时的载体,这个接口非常的简单就一个静态的创建方法+三个初始化、处理运行时消息和关闭消息循环的纯虚函数组成。

  • [x] A、WeWorkRuntimeImpl继承了接口WeWorkRuntime,是其一个具体的实现。其中接口的静态创建方法就是创建一个WeWorkRuntimeImpl的裸指针。这个WeWorkRuntimeImpl在cpp实现文件中同时完成了成员变量、方法的定义和实现。原来可以这样用,以前一直是接口一个.h文件,实现类分别有.h和.cpp文件。
  • [x] B、成员变量方面,三个bool分别描述,运行时是否初始化完毕、是否已经关闭和是否创建了非UI线程。一个AtExitManager的智能指针用来控制所有从singleton模板创建的单例的生命周期,所有的单例在其析构的时候进行析构,如果AtExitManager不进行初始化,则singleton模板的继承类创建也会失败。第一章 · 第一节中提到的WeWorkMainDelegate*也是其一个成员变量
  • [x] C、NotificationService未研究,待续
  • [x] D、Initialize方法完成了各个成员变量的初始化,给android平台下需要预先设定sqlite3_temp_directory这个全局变量的值,否则sqlite的初始化会失败。调用第一章 · 第一节提到的delegate的为各个阶段准备的预处理函数(虽然我们完全没有用到这些函数如(SandboxInitialized等)。然后对WeWorkMainLoop对象main_loop_进行赋值、初始化、再初始化、启动(Init()、EarlyInitialization()、InitializeToolkit()、MainMessageLoopStart()),在最后创建其他的非UI线程并检查有无错误。这里有个小疑问在最后成功return前,为什么调用了signal(SIGPIPE, SIG_IGN);意义何在啊?(转:当服务器close一个连接时,若client端接着发数据。
  • [x] 根据TCP 协议的规定,会收到一个RST响应,client再往这个服务器发送数据时,系统会发出一个SIGPIPE信号给进程,告诉进程这个连接已经断开了,不要再写了。
  • [x] 根据信号的默认处理规则SIGPIPE信号的默认执行动作是terminate(终止、退出),所以client会退出。若不想客户端退出可以把SIGPIPE设为SIG_IGN),从描述可以看到因为我们的APP是单进程的所以这一行的意义并不大。
  • [x] E、Run()和Shutdown()其实更多的是对WeWorkMainLoop对象main_loop_的一些方法的组合调用。Run()里面调用的是WeWorkMainLoop的RunMainMessageLoopParts(),Shutdown()调用的是WeWorkMainLoop的ShutdownThreadsAndCleanUp(),并进行了一些变量的反初始化和析构的工作。
  • [x] F、RunMainMessageLoopParts()函数在iOS下,因为主线程的消息循环我们不能接管,故而只能采取Attach的方式,把一些消息循环中需要用到的变量、对象告诉给pump。真正的主线程消息循环是UIApplicationMain函数。

接下来我们进入到整个框架最关键、最灵魂也最复杂的,处理整个APP进程消息的WeWorkMainLoop。

消息循环:src/common/wework_main_loop.h src/common/wework_main_loop.cpp

这个类以及下一节要介绍的这一组类说夸张一点,是任何平台的客户端类程序的心脏,它为进程提供了可用的线程,驱动程序相应各类外部消息,管理进程类各个对象的生命周期。当客户端程序员已经熟练掌握了UI、逻辑、网络、数据、兼容层以及等等客户端的实现以后。就像医学院里,成绩最好的、最有理想的学生会选择外科,这部分佼佼者中的金字塔尖的学生会选择胸外科对心脏动手术一样。对消息循环的掌握对客户端程序员尤其重要。
这个类极其复杂,这里会抽丝剥茧,只讲其最精华的部分。

  • [x] A、WeWorkMainLoop* g_current_browser_main_loop 每个进程都会有一个全局唯一的全局变量。构造函数、析构函数分别对其初始化和反初始化
  • [x] B、EarlyInitialization()函数进行各种需要提前准备的工作,比如根据命令行启动各种监控,给windows程序初始化socket。wework在这里没有需要做的工作。
  • [x] C、MainMessageLoopStart()函数判断当前的线程是否有一个base::MessageLoop绑定,如果没有就初始化 scoped_ptr main_message_loop_,并调用InitializeMainThread()函数将当前主线程命名,把成员变量 scoped_ptr main_thread_用当前线程初始化。有意思的是,如果去搜索一下这个变量你会发现,main_message_loop_除了在这里被reset赋了初始值,它就没有在其他任何地方被引用了。这是说它的存在是没有意义的吗?当然不是!具体它的作用请参考第一章 · 第四节。如果是安卓平台,还需要主动调用 base::MessageLoopForUI::current()->Start() 去进行初始化???

底层的消息循环:src/third_party/base/message_loop/ (主目录) message_loop.h message_loop.cc message_pump.h message_pump_mac.h.mm message_pump_libevent.h.cc message_pump_default.h.cc

这一部分是更底层的消息循环。从上面提供的涉及到的文件名称我们可以看到,pump就是那个驱动程序生生不息的心脏。接下来我们回答上一节留下的问题。要回答这个问题我们就要去base::MessageLoop的构造函数去看看它做了什么工作。不过在看之前,我们还是先简单分析一些这个类的构成

  • [x] A、base::MessageLoop的父类是MessagePump::Delegate,从继承关系就能够猜到base::MessageLoop是MessagePump的一个delegate,帮助MessagePump来完成一些辅助的工作。如果我们具体的去看看MessagePump::Delegate的实现,这个delegate由三个纯虚函数加一个定义为空的虚函数构成。三个纯虚函数都和“执行工作”有关。分别是 DoWork() DoDelayedWork(TimeTicks* next_delayed_work_time) DoIdleWork(),从名字可以看出它们就是来真正执行各种Task的函数(Task请参看下一节),我们接下来就要仔细分析这三个函数,不过无本之木无源之水,我们还是从构造函数开始。

  • [x] B、base::MessageLoop的构造函数还是惯例的初始化成员变量以及调用构造函数。type_说明base::MessageLoop是UI或DB或IO。nestable_tasks_allowed_说明在一个Task执行中能否处理这个Task产生的新Task,默认是允许的。这里还有第三个成员变量是RunLoop* run_loop_,不过我们看见构造函数并没有初始化这个变量。实际上这个变量就是对它所在的base::MessageLoop的消息循环函数族的一个封装类。由这个类来负责消息循环生命周期、嵌套深度等运行信息。特别强调一点,iOS主线程其实没有用到这个变量(大雾)。

  • [x] C、构造函数里面除了初始化变量,还做了两件重要的事情,调用Init()函数以及创建了scoped_ptr pump_成员变量。这里需要注意的是pump_是protected继承的。从构造函数我们可以看出MessagePump是在它的delegate中创建的(F点会继续解释如何创建的)。

  • [x] D、在Init()函数中,代码首先就是一句 lazy_tls_ptr.Pointer()->Set(this); 这一行代码能够被单独提出来进行说明,因为 LazyInstance >::Leaky lazy_tls_ptr 这个变量用到了LazyInstance延迟创建模板。所谓延迟创建就是当这个类对象真正被使用到了也就是说lazy_tls_ptr的Pointer()方法或Get()被调用的时候,才采用POD的方式(http://en.wikipedia.org/wiki/Plain_old_data_structure),把对象的真实数据通过拷贝的方式,赋值给堆上在程序启动前就分配好空间的地址上。用全局变量提前在堆上分配空间,不仅速度更快还能有效的避免堆上过早的产生碎片。所以看chromium的代码,LazyInstance的对象都是全局对象,不过现在的代码已经删除了很多与chrome浏览器业务逻辑相关的LazyInstance全局对象了。话说回来,这个对象封装的是Thread Local Storage,每个平台都略有区别,POSIX是用pthread_setspecific。

  • [x] E、在Init()函数中,会创建一个IncomingTaskQueue用来接收向这个线程输入的Task。这个类继承了RefCountedThreadSafe,这说明了两点:一、这个变量会被多个线程使用到。二、因为用到了引用计数,说明这个成员变量的生命周期是长于它所在的类的,这点也很好理解,当要销毁base::MessageLoop的时候,可能还有Task在输入队列中。我们在这里简单分析一下IncomingTaskQueue的实现,它就是一个输入Task队列的维护管理类。把锁放入这个对象,这样Get和Set看起来就很整洁了。紧接着函数会为message_loop_proxy_成员变量初始化创建一个MessageLoopProxy接口的实例。看起来很高深的用到了设计模式中的Proxy模式,确实也是。从MessageLoopProxy的构造函数可以看到传入了incoming_task_queue_成员变量,实际上Proxy就是对incoming_task_queue_的一个访问控制和封装,提供刚出来的三个虚函数接口,在其内部的实现都是调用incoming_task_queue_->AddToIncomingQueue把Task加入到输入队列中。注意不要被proxy的名字给骗了,这个proxy其实只代理了message_loop_对象添加Taks这个功能族的相关方法。正因为如此,其类的结构也和教科书上的UML结构有区别,这种改变将message_loop_真正需要开发给其他线程调用的接口,不仅进行了很好的封装(单独的父类),也把需要暴露的部分尽可能的最小化了。这样使用者在使用时,将无法对这个重要类的其他重要功能造成任何影响。

  • [x] F、ThreadTaskRunnerHandle包含一个SingleThreadTaskRunner成员变量,这个类也是MessageLoopProxy的父类,其实代码就是传的MessageLoopProxy变量的地址进去的。ThreadTaskRunnerHandle对象会把自己的地址装到TLS里面,这样当timer定时器到时间的时候,就会把TLS里面的ThreadTaskRunnerHandle对象地址取出来,就是上面提到的MessageLoopProxy变量。然后调用变量的三个纯虚函数,把Task放入输入队列。记住只有Timer触发的Task任务是通过ThreadTaskRunnerHandle加入到输入队列的。结合上面的E点,非Timer的Task中的一部分,会通过message_loop_proxy_把任务放入对应线程的输入队列等待执行。剩余的Task则通过第一章 · 第六节的WeWorkThread接口把Task添加到输入队列。

  • [x] G、创建MessagePump是一个MessageLoop的静态方法CreateMessagePumpForType(Type type)。从实现我们可以看到从线程类型的角度来说,对于iOS的程序来说,一共有三类:UI线程、IO线程和其他线程,分别对应的Pump也不同为MessagePumpMac、MessagePumpLibevent和MessagePumpDefault。

  • 这一节写到这里才能回答上一节留下的问题 Q:“WeWorkMainLoop类中的main_message_loop_只被初始化了,但没有被任何地方‘引用’,为什么?或者说如果不初始化main_message_loop_为什么程序就不能正常工作了?”A:main_message_loop_构造函数中创建了incoming_task_queue_,并以此为基础创建了message_loop_proxy_和thread_task_runner_handle_。这三个变量是线程消息循环核心。没有他们的支持任何自定义的Task都无法执行,所有需要跨线程的地方都无法正常执行了。说到这里又引入了一个疑问,你介绍的这三个变量都是辅助支撑消息循环。那么真正的消息循环又是怎么跑起来的?好像不解释清楚这个问题,就没办法说清楚,谁在用这些变量来驱动程序工作。其实这些工作都是pump_变量来完成的。接下来我们继续分析pump_的工作

  • [x] H、iOS的主线程用到的pump是MessagePumpMac和MessagePumpUIApplication。前者只包含一个静态创建MessagePump*对象指针的静态函数,而这个指针指向的就是后者说定义的对象。MessagePumpUIApplication因为iOS的消息循环不能接管,并没有真正的Run()函数。当有新的Task需要主线程执行的时候,先加入到incoming_task_queue_队列中,然后通过CFRunLoopSourceSignal(...)激活我们自定义的Task处理函数RunWorkSource()。因为没有MessagePumpUIApplication::Run()函数(虽然有,但是不会被调用,内部实现也是空的),所以在main.mm中我们会显示的调用WeWorkRuntimeImpl的Run()函数,在这个函数内部会调用MessagePumpUIApplication::Attatch()初始化run_loop_。虽然run_loop_在iOS主线程中并没有什么用,但因为MessagePump和message_loop中的函数实现是为所有线程服务的,如果不调用Attach()初始化run_loop_,很多DCHECK都无法通过。还记得B点那个大雾吗?我们发现在MessagePumpUIApplication里面也有个run_loop_变量。这个类只给iOS的主线程使用,虽然所有的pump中都有一个同名的run_loop_变量,但这个类的run_loop_变量的类型却与众不同是CFRunLoopRef。它其实就是当前iOS主线程消息循环的一个引用,我们用它给iOS主线程消息循环加上我们自己的函数到消息循环中,当条件满足的时候(没有UI优先的事件需要处理、Timer到时、主动signal激活等等),对应的自定义函数即被调用。

  • [x] I、普通线程用到的pump是MessagePumpDefault,在Run()函数中依次调用loop的DoWork() DoDelayedWork(...) DoIdleWork()三个函数。如果有没有延迟执行的Task,则线程会等待在一个event上,其他线程把Task放入本线程的incoming_task_queue_后,会signal这个event,让本线程的Run()函数跑起来执行Task。相反,如果有延迟执行的任务,线程的event就会用当前时刻到最近一个延迟Task的时间间隔作为等待时间进行等待。看了上面的介绍,我们能够发现MessagePumpDefault的ScheduleWork()可以在任意线程被调用用来激活当前线程进行工作。而MessagePumpDefault的ScheduleDelayedWork()一定是在当前线程被触发的,因为在这个函数的内部仅仅是计算延迟任务需要延迟的时间。

  • [x] J、IO线程负责网络的收发,他用到的pump是MessagePumpLibevent。通过名字可以看到它用到了LibEvent库。LibEvent库是一个跨平台的网络库,它为各个平台封装了最适合平台的网络收发的实现。它的具体实现请参考专门的介绍文章。因为是IO线程,其signal的方式是通过建立一对pipe的fd。让IO线程检查输入fd是否可读,一旦可读事件发生,就处理输入队列的Task事件。

线程和Task:src/third_party/base/threading/thread.h src/common/wework_client_thread.h src/common/wework_client_thread_impl.h src/common/wework_client_thread_impl.cc

我们先来看线程方面的接口,一共两个 WeWorkThread 和Thread。前者提供了一组跨线程分发任务的接口,后者则提供了对线程本身的一个封装。

  • [x] A、WeWorkThread提供了一组PostXXXTask的静态方法,这些方法会调用WeWorkTHreadImpl::PostTaksHelper(...)。后者其实就是就是根据参数确定需要执行的Task的线程tid,然后从保存各个线程的message_loop_全局数组中找到对应的loop_,然后用loop_的PostXXXTask的方法把任务加入输入队列。不过这里因为用到了全局数组,必须要加锁,所以用WeWorkThread接口,整个Post的过程会加锁两次。所以推荐用message_loop_proxy_来发送Task。
  • [x] B、Thread则是一个线程的管理类。对线程的生命周期、名字和loop的关联等提供服务,本身没有多少对外提供的接口可供使用。

任务队列:src/third_party/base/message_loop/incoming_task_queue.h.cc

这个类用户维护输入(PostTaskXXX)的Task,因为Task多来自于其他线程,在Task进入队列的时候需要加锁。但整个APP进程内部的跨线程执行,仅仅只在Task进入队列的时候加锁,不需要程序员加锁。程序员只需要关注代码的逻辑,已经逻辑需要在合适的地方执行即可。这种跨线程只在进入队列的时候加锁一次的实现方法非常巧妙值得推荐,网上有很多很详细的文章深入的介绍这里的具体实现,建议大家找来看看。

  • [x] A、incoming_queue_lock_唯一的一个负责跨线程调用同步的锁。incoming_queue_是存放的Task的std::queue队列。和队列关系最密切的无疑是执行队列里面Taks的消息循环,所以message_loop_就是那个队列对应的消息循环。next_sequence_num_是用来非顺序执行Task的时候,取出对应的num的Task进行执行。
  • [x] B、AddToIncomingQueue就是把各种PostTask过来的Closure保存起来放到incoming_queue_里面。如果是delay执行的Task还需要根据FIFO的规则,为其记录一个sequence号。当delay的时间结束需要开始执行Task的时候,可能同时会有多个Task满足执行的条件,就需要根据sequence号区分先来后到。
  • [x] C、加入到incoming_queue_完毕以后,需要调用base::MessageLoop的ScheduleWork方法。这个方法根据不同的平台,不同的线程的类型有着不同的实现,但一般都叫signal,一般是在非执行线程被调用的。详情请参考第一章 · 第四节。ScheduleDelayedWork方法上面也提到过,只是把延迟的最短时间记录下,线程消息循环会检查时间是否过期,过期就执行对应的Task。这个方法是在本线程调用的。

可等待事件 : src/third_party/base/synchronization/waitable_event.h.cc

对于可等待的通知事件来说,“等待”部分实现的核心就是用锁来进行实现。在POSIX标准下,通过pthread_mutex_t来实现锁的功能。“通知”部分实现的核心则是通过条件变量来实现。在POSIX标准下,通过pthread_cond_t来实现通知的功能,pthread_cond_t需要一个pthread_mutex_t的配合来完成通知的功能。

主要的流程是

pthread_cond_init(&cond) //初始化pthread_cond_t变量
pthread_mutex_lock(&mutex) //锁住pthread_mutex_t变量
WaitableEvent //调用SyncWaiter记录下当前线程(等待线程)的相关信息
pthread_cond_wait(&cond, &mutex) //首先解锁mutex,然后等待cond被signal激活(线程X)
pthread_cond_signal(&cond) //将会signal激活等待在这个条件变量上的线程(并不是调度这个线程执行),并锁住mutex
pthread_mutex_unlock(&mutex) //解锁pthread_mutex_t变量

最后线程X,被OS调度后会执行wait后续的代码。POSIX为什么会前后两次lock、unlock呢?因为可等待的通知事件是可以被用在多个线程中,这些线程可以同时等待某个事件的完成。那么wait函数就需要是线程安全的,但wait函数要把等待线程的信息和事件关联起来,以及事件signal以后把线程信息和事件解除关联。所以就需要lock、unlock两次。

你可能感兴趣的:(chromium代码学习)