Chrom 的线程模型

Wesley13
• 阅读 731

线程(http://www.chromium.org/developers/design-documents/threading )

a) 概述

Chromium是一个超级多线程的产品,我们尝试让UI的反应尽可能的快,这样就意味着不要用任何的I/O操作或者长操作来阻塞UI 线程,我们的方法是在线程之间使用消息传递,我们不鼓励使用阻塞和线程安全的对象,取而代之的是,对象都只存在一个线程中,我们在线程间传递消息通讯,为大部分的跨线程请求使用callback接口返回结果。

Thread 对象定义在 base/thread.h 中,一般来说,你都可以使用这里已经有的线程,而非自己去重写一个新的。我们已经有太多的线程,以致我们很难去跟踪。每条线程都有一个 MessageLoop (base/message_loop.h)来处理这个线程的消息,你可以获取每个线程的MessageLoop,用这个函数就行 Thread.message_loop() 。

b) 已有的线程

大部分线程都被 BrowserProcess 对象管理,BrowserProcess对象就像是“browser”进程的服务管理者一样,一般所有的事情都发生在UI线程上(也就是程序开始的主线程),我们将某些类型的处理压进这些线程里,以下这些线程都可以获取接收这些处理:

io_thread:分发线程,处理 browser 进程和 子进程之间的通信,也是所有资源请求(webpage loads)调配的地方。

file_thread:一个处理文件的线程, 当你想做一些阻塞的文件系统的操作时,派遣到这个线程。

db_thread:数据库操作的线程,例如,cookie service 会在这个线程里做一些 sqlite 的操作。注意,history 不会使用这个线程。

safe_browser_thread

有几个组件拥有它们自己的线程:

history:history service 有自己的线程,其实它可以合并到 db_thread 中,不过,我们需要确定事情发生的精确顺序,例如,cookies load会先于 history load,因为cookies需要被先读,而且 history 的初始化比较长并且是阻塞的。

Proxy service :net/http/http_proxy_service.cc. 

Automation proxy :这个线程用来和 UI test 程序通信,驱动应用。

c) 让浏览器保持良好的响应性

在概述中可以看到,我们避免在UI线程中做任何的阻塞IO操作,以便让UI保持良好的响应性,另一个隐藏的意思是,我们也需要避免在IO 线程里做阻塞的IO 操作,原因是,如果我们在IO 线程中做耗时操作,例如磁盘读写,那么IPC 消息就不会被及时处理,会影响到用户和页面的交互,我们可以用 异步IO或者完成端口 来做这个事情。

另外一个需要注意的是,不要线程间彼此阻塞,锁只能用于多线程间共享的数据结构,如果一个线程执行比较耗时的操作或者磁盘操作的时候,就应该放开锁,只有得到结果的时候,才使用锁来交换新数据。这里是一个例子:PluginList::LoadPlugins (src/webkit/glue/plugins/plugin_list.cc) ,如果你非要用锁,这里是一些比较好的实践帮助你避免一些陷阱(http://www.chromium.org/developers/lock-and-condition-variable )。

为了写 non-blocking 的代码,chrome的很多 API 都是异步的,这也意味着,这些代码可能是运行在某个线程中,然后通过某些委托接口返回;或者他们会在请求操作完成时候会触发一个base::Callback<> 对象。执行操作会在下面的 PostTask 章节中说到。

d) 线程中的技术点

i. base::Callback<>, Async APIs, and Currying

base::Callback<>(callback.h ) 是一个模版类,有一个Run函数,这个函数是函数指针的泛化,这个Callback对象是被 base::Bind 函数创建的,异步API经常会采用 base::Callback<> 作为一种手段来异步返回操作的结果,下面是一个例子:

void ReadToString(const std::string& filename, const base::Callback<void(const std::string&)>& on_read);

void DisplayString(const std::string& result) {

LOG(INFO) << result;

}

void SomeFunc(const std::string& file) {

ReadToString(file, base::Bind(&DisplayString));

};

在这个例子里,base::Bind 将 DisplayString 这个函数变成了 base::Callback<void(const std::string& result)> 对象,base::Callback<> 对象的类型决定于参数。为什么不直接传函数指针呢?这是因为 base::Bind 允许调用者去适配函数接口,并且通过 Curring(http://en.wikipedia.org/wiki/Currying ) 附上一些额外的内容,例如,我们已经有一个工具函数 DisplayStringWithPrefix 可以用参数作为前缀,我们使用 base::Bind 来适配这个接口如下:

void DisplayStringWithPrefix(const std::string& prefix, const std::string& result) {

LOG(INFO) << prefix << result;

}

void AnotherFunc(const std::string& file) {

ReadToString(file, base::Bind(&DisplayStringWithPrefix, "MyPrefix: "));

};

这可以用来代替创建一个适配函数,拥有一个前缀成员变量的小类。还要注意的是“MyPrefix:”参数实际上是一个const char *,而DisplayStringWithPrefix实际上想要一个const  std::string。像正常功能调度,base::Bind,将强制转换参数的类型,如果可能的话。请参阅“ base:: bind()如何处理参数” 会讲更多的细节,关于参数存储,复制和特殊处理的参考。

ii. PostTask

分配给另外线程最底层的函数是使用 MessageLoop.PostTask 和 MessageLoop.PostDelayTask( base/message_loop.h)。

PostTask分配了一个task在指定的线程中运行,task 被定义为 base::Closure, 

base::Closure 是一个 typedef :

Typedef  base::Closure  base::Callback<void(void)>

PostDelayedTask 分配一个task给指定线程,这个task会延迟一段时间再执行,一个task被typedef 为 base::Closure,这个定义实际是一个Callback对象,包含了一个Run函数,这个对象被 base::Bind()函数创建,处理一个task,message_loop最终会调用 base::Closure 的 Run 函数,然后会把task 对象的引用 drops。

PostTask 和 PostDelayTask 都有一个tracked_objects::Location 的参数,这个参数是为了满足轻量级的调试用途(task计数、正等待的task以及已完成的task都可以通过Url about:objects被监测到,当然我们需要debug版本)。一般来说,FROM_HERE 宏是这个参数的适当的值。

注意,新task在message_loop里运行,我们指定的任何delay值,都会受制于操作系统的timer 精度,这就意味这,在windows下,非常小的timeout,例如10ms以下,将有可能无法实现(delay会更长)。使用0作为PostDelayedTask 的参数,就等价于直接调用 PostTask。

PostTask也用来在当前线程中做一些事情,有时候在message_loop的当前处理返回之后。这样一个在当前线程的延续操作,将可以用来保证这个线程中的其他关键任务不会被“饿死”。

下面的例子是为一个函数创建一个task,并post它到别的线程(这个例子是 post到 file thread):

void WriteToFile(const std::string& filename, const std::string& data);

BrowserThread::PostTask(BrowserThread::FILE, FROM_HERE,
base::Bind(&WriteToFile, "foo.txt", "hello world!"));

你应该总是使用 BrowserThread 在线程间投递task,不要缓存MessageLoop的 指针,因为这会造成一些bug,例如这些指针已经被删除了,你却还在使用他们。更多的信息可以参考这里:http://www.chromium.org/developers/design-documents/threading/suble-threading-bugs-and-patterns-to-avoid-them 

iii. base::Bind() and class method

base::Bind API 也支持调用类方法,语法非常类似 base::Bind 一般的函数,除了第一个参数是类对象,默认情况下,PostTask 使用的对象应该是线程安全、使用引用计数的对象,引用计数保证对象在另一个线程中调用将继续存在,直到task完成。

class MyObject : public base::RefCountedThreadSafe {
public:
void DoSomething(const std::string16& name) {
thread_->message_loop()->PostTask(
FROM_HERE, base::Bind(&MyObject::DoSomethingOnAnotherThread, this, name));

}

void DoSomethingOnAnotherThread(const std::string16& name) {
...
}

private:
// Always good form to make the destructor private so that only RefCountedThreadSafe can access it.
// This avoids bugs with double deletes.
friend class base::RefCountedThreadSafe;

~MyObject();

Thread* thread_;
};

如果你有一个外部的同步结构,并且你可以完全肯定这个对象会在task等待执行的过程中一直存在,那么你可以在调用 base::Bind() 的时候使用 base::Unretained() 封装这个对象指针,这样可以关闭引用计数。这个也允许用在不用引用计数的类中,不过当你这样做的时候,要非常小心。

iv. base::Bind() 如何处理参数

参数传递给 base::Bind() 的时候会拷贝到一个内部的 InvokerStorage 结构对象( base/bind_internal.h)。当这个函数执行完的时候,能看到参数的拷贝。如果你的目标函数或者方法使用一个 const 的引用,这就非常重要了。如果你需要一个原始参数的引用,你可以使用 base::ConstRef() 来包装这个参数。小心使用这个,因为如果你不能保证这个引用直到task执行完成之后都存在,那就会很危险了。尤其是一个变量在堆上创建的时候,使用 base::ConstRef() 是很不安全的,除非你能保证这个堆一直有效,直到完成这个异步task。

有时候,你会想传一个引用计数的对象作为参数(记得使用 RefCountedThreadSafe 和不纯的RefCounted 作为基类),保证对象在整个请求的过程中都存在,base:Bind 生成的 Closure 应该保持一个引用,这可以在传递 scoped_refptr 作为参数类型或者使用 make_scoped_refptr 包装原始指针的时候来做这个事情。

class SomeParamObject : public base::RefCountedThreadSafe {
...
};

class MyObject : public base::RefCountedThreadSafe {
public:
void DoSomething() {
scoped_refptr param(new SomeParamObject);
thread_->message_loop()->PostTask(FROM_HERE
base::Bind(&MyObject::DoSomethingOnAnotherThread, this, param));

}

void DoSomething2() {
SomeParamObject* param = new SomeParamObject;
thread_->message_loop()->PostTask(FROM_HERE
base::Bind(&MyObject::DoSomethingOnAnotherThread, this,

make_scoped_refptr(param)));

}

// Note how this takes a raw pointer. The important part is that

// base::Bind() was passed a scoped_refptr; using a scoped_refptr

// here would result in an extra AddRef()/Release() pair.

void DoSomethingOnAnotherThread(SomeParamObject* param) {

...
}
};

你如果想直接传对象而不是传它的引用,那你可以用 base::Unretained(). 再次提醒,使用这个需要非常的小心。

如果你的对象有一个实际的析构函数,需要运行在某个线程里,你可以使用下面的这个trait,这个是需要的,因为一个时间先后的问题会导致一个task在代码投递之前完成执行,这样不会破坏堆栈。

class MyObject : public base::RefCountedThreadSafe<MyObject, BrowserThread::DeleteOnIOThread> {

...};

v. base::WeakPtr 和 Cancellation

我们有时候会在我们的对象“之后”干一些事情,比如你用 PostDelayTask,当你的task被调用的时候,却发现你的对象已经被删除了,这种事情导致大量的Crash,尤其是在程序退出的时候。

在这种情况下,你可以用 base::WeakPtr 和 base::WeakPtrFactory ( base/memory/weak_ptr.h ) 来确定任何的调用都不能发生在对象的生命周期之外,这里没有使用引用计数。base::Bind 函数机制会特别清楚 base::WeakPtr 对象,如果这个对象无效的时候,base::Bind 会停止task的执行。

base::WeakPtrFactory 用来生成多个 base::WeakPtr 实例,当 Factory 对象销毁,则所有的 WeakPtr 会将他们内部的 "invalidated " 标志置为true,这样会导致绑定在他们身上的task不能分发。(task可以绑定在 WeakPtr 上面),将 factory 作为分发对象的成员变量,你可以拥有这个“自动消除”的属性。

class MyObject {
public:
MyObject() : ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)) {
}

void DoSomething() {
const int kDelayMS = 100;
MessageLoop::current()->PostDelayedTask(FROM_HERE,
base::Bind(&MyObject::DoSomethingLater, weak_factory_.GetWeakPtr()),
kDelayMS);
}

void DoSomethingLater() {
...
}

private:
base::WeakPtrFactory weak_factory_;
};

vi. Cancelable request (可取消请求)

可取消请求让它能轻松的向别的线程提出请求,然后那个线程异步给你返回一些数据。就像可撤销的店铺系统一样,我们使用对象,这些对象可以跟踪它的源对象是否存在,如果调用对象已经被删除了,那这个请求也会取消,以避免无效的回调。

像可撤销的店铺系统一样,一个可取消请求的用户拥有一个对象(这里称之为 "Consumer"),它跟踪对象是否还存在,并且会自动删除任何未完成的请求。

class MyClass {
void MakeRequest() {
frontend_service->StartRequest(some_input1, some_input2, this,

// Use base::Unretained(this) if this may cause a refcount cycle.

base::Bind(&MyClass:RequestComplete, this));

}

void RequestComplete(int status) {
...
}

private:
CancelableRequestConsumer consumer_;
};

注意这个MyClass::RequestComplete ,是和 base::Unretained(this) 绑定在一起了。

Consumer 允许你附加额外的数据在一个请求上,使用 CancelableRequestConsumer 将允许你附加任意的数据,这些数据当你调用请求的时候,会被 provider service 返回。当请求取消,数据将会自动销毁。

一个处理请求的服务继承自 CancelableRequestProvider, 这个对象提供了可以取消轻量级请求的函数,并且将会和 consumers 一起保证在取消的时候,所有东西都被正确的清理。这个 frontend service 只是跟踪并且发送到另外一个线程上的 backend service,它看起来就像这样:

class FrontendService : public CancelableRequestProvider {
typedef base::Callback<void(int)> RequestCallbackType;

Handle StartRequest(int some_input1, int some_input2,
CallbackConsumer* consumer,
const RequestCallbackType& callback) {
scoped_refptr< CancelableRequestFrontendService::RequestCallbackType >

request(new CancelableRequest(callback));

AddRequest(request, consumer);

// Send the parameters and the request to the backend thread.
backend_thread_->PostTask(FROM_HERE,
base::Bind(&BackendService::DoRequest, backend_, request,

some_input1, some_input2), 0);

// The handle will have been set by AddRequest.
return request->handle();
}
};

backend service 在另一个线程中运行,它处理并把结果转发回原始的调用者,如下:

class BackendService : public base::RefCountedThreadSafe {
void DoRequest(
scoped_refptr< CancelableRequestFrontendService::RequestCallbackType >
request,
int some_input1, int some_input2) {
if (request->canceled())
return;

... do your processing ...

// Execute ForwardResult() like you would do Run() on the base::Callback<>.
request->ForwardResult(return_value);
}
};

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Easter79 Easter79
2年前
swap空间的增减方法
(1)增大swap空间去激活swap交换区:swapoff v /dev/vg00/lvswap扩展交换lv:lvextend L 10G /dev/vg00/lvswap重新生成swap交换区:mkswap /dev/vg00/lvswap激活新生成的交换区:swapon v /dev/vg00/lvswap
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
2星期前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
2年前
Java获得今日零时零分零秒的时间(Date型)
publicDatezeroTime()throwsParseException{    DatetimenewDate();    SimpleDateFormatsimpnewSimpleDateFormat("yyyyMMdd00:00:00");    SimpleDateFormatsimp2newS
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
6个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这