client多线程

时间:2023-03-08 19:07:48

1.多线程对象





对象可以是多线程访问,线程可以在这里分为两类:

为完成内部业务逻辑的创建Thread对象,线程需要访问对象。

使用对象的线程外部对象。

进一步假设更精细的划分。业主外螺纹成线等线,。

在此基础上,能够看看对象的生命周期。

而生命周期的開始是easy确定的,可是对象生命周期在哪个线程上结束?





1.1对象能够在内部线程上析构吗?

假设内部线程是完毕业务逻辑。则对象不适合在这种线程上析构,这样带来的逻辑关系就是

对象拥有线程,线程又控制对象的生命周期。好点的做法应该是对象在生命周期终止的时候,

中止这些内部线程。假设内部线程是一个GC线程,也就是设计中专门用来析构对象的,这取

决于详细设计(比方为性能考虑,专门用一个线程来回收资源)。

1.2对象的并发性。

对象的并发性是指能否够在多线程中调用对象的方法。同意多线程调用时有两种模式。一种

是每一个线程中直接调用对象的方法,还有一种是在一个线程中直接调用对象的方法在其他线程拥

有对象的代理。通过代理调用对象的方法时动作在能直接调用方法的线程内运行。





第一种模式要求对象提供的服务接口具有线程安全性。会提高对象的复杂度。一般都通过锁来

实现。假设有多个锁,就须要注意按一定顺序获取锁。假设在某个线程上获取锁后发生异常,

整个对象就杯具了。





而另外一种模式仅仅须要觉得对象在单线程中工作,可是须要额外的跨线程调用的机制。

该模式有

一个简单的实现。调用方法时创建一个事件。然后生成一个闭包(事件在闭包中),通过消息

把闭包传到目标线程(一般要求目标线程有消息循环或EventLoop),目标线程完毕工作后。把

结果放在闭包中,然后激发事件。在调用者线程中,能够同步等待,也能够等待超时。这不就

是Future模式么?





1.3对象的析构策略。

在第一种模式中,能够在最后一个拥有引用计数的线程中析构,也能够将析构动作强行推到固定

线程中析构(仅仅考虑固定线程为拥有者线程的情况)。

[假设再时髦一点,能够增加弱引用(弱指针)。在须要使用该对象提供的服务时,提升为强指针。

细致想想,在生命周期问题上和直接用强指针没啥大差别。

直接使用强指针须要在该线程拥有这

个对象的引用时,加一个引用计数,而弱指针直到该线程须要訪问对象时才提升为强指针,添加

引用计数。对对象所在的析构线程没啥影响。对象仍然可能在随意的外部线程析构。]





假设在最后一个拥有引用计数的线程中析构,析构时会释放对象拥有的资源,也许,依赖于其他

资源,这就要求拥有的其他资源的线程安全性有一定保障,须要该对象知道这些资源的线程安全

特征。

依赖组合在一起会提升系统复杂度。

使用其他技术或许能解决一些依赖,可是系统复杂度

还是比較大。所以,在系统复杂的情况下,倾向于使用在固定线程中析构的策略,也就要求有在

某个线程中运行析构函数的机制。

在另外一种模式中。因为非拥有者线程拥有的是代理。在语义上不会影响对象的生命周期,也就是

说代理生命周期能够大于对象生命周期,仅仅是在调用方法时发现对象已经销毁而已。因此该模式

下一般觉得析构动作在拥有者线程中。





通过两种模式的比較,我们都发现须要区分拥有者线程和使用者线程。

当中在模式一中,拥有者

线程的存在性主要是析构时的依赖所引入的,否则,除去拥有者创建对象以外,拥有者线程和使

用者线程没有差别。

在两种模式的比較中,隐隐约约都出现了在指定线程上调用方法的动作。









1.5考虑一下使用者怎样调用对象的方法。

一般而言,使用者是业务逻辑。(对象本身也是业务逻辑的一种,仅仅是可能处在较低的层次而已)

假设某个业务须要开线程或者使用已知线程,往往在同一时刻。逻辑仅仅在一个线程上工作。

看一个样例。依据某本书从server拉取和书相关的推荐信息。拿到本地的书的信息须要调用某个对象

的方法,我们在业务发起线程中拿到书的信息,在IO线程中下载,然后到COMPUTE线程中解析结果,回

到发起线程中发出推荐消息。整个业务分为几个阶段,同一个阶段仅仅存在于一个线程。而对书的信息

的获取。仅仅在最開始获取一次,不是等到IO线程中再去查询本地的书的信息。

我们的本地的书的信息

仅仅能在一个线程中訪问。

client的业务中难以遇到平等的多线程(平等的多线程的样例是完毕port的服务线程)。

开多线程

的意义在于不会堵塞线程。而为了实现这点,应该把业务分为若干步,每一步放到一个线程里去做。

在业务逻辑的层次上做这种拆分往往是可行的。除非对象提供的服务足够底层,面向的不是业务

逻辑,而是随时都可能用到该服务(比方面向语言。提供存储分配服务)。除非业务逻辑对结果的

实时性有较高要求:





设主逻辑线程为0,业务逻辑A须要服务对象S的服务。A调用S的服务后得到结果R,切换到线程1。

在线程1上工作的时候,S的状态发生变化,假设调用S,得到Rx,假设依赖于结果R进行处理否则这

个业务逻辑就会产生严重后果。这种情况非常少。





所以:单个对象提供的服务。大多数时候其使用者仅仅在同一个线程里使用服务,服务本身面向单线程。

所以。从使用者的角度来看,对象面向单线程是足够用的,可是也须要在指定线程上调用方法的机制:

业务逻辑本身的线程切换。

所以我们都希望有机制可以在指定线程上调用方法。





1.6怎么实如今指定线程上调用方法。

a.全部的线程都有某个循环,比方任务循环。

b.实现派发器,派发器可以向不论什么线程派发任务。

c.人为地将一个线程标记为主线程。也就是master。其他线程是worker。在master线程中运行业务

逻辑,必要的时候将运行的一部分放到worker中完毕后回到master线程或者再次放到其他worker线程中。

派发器能够是多线程訪问的:在随意线程都能够派发任务的动作。派发器也能够是单线程訪问的。

比方仅仅能在master线程中訪问。这个时候仅仅须要提供将任务派发到worker线程的方法。

可是这样就

缺乏从worker线程切换到master线程的机制。这个时候要么我们不关心worker线程中的运行结果,要么

能够在master线程中轮询结果,或者直接在worker线程中回调(比較危急)





1.7一个未提及的case

尽管单个业务的每一步仅仅存在于一个线程上。可是毕竟存在多线程訪问,假设在worker线程中运行

的时候,任务拥有的资源,业务逻辑的回调接收者已经销毁了呢?因此。在设计任务的时候。应该

具有整个任务上下文的拷贝,这样就不存在资源在别的线程中被释放的问题。而对于回调的接收者

可能销毁的问题。仅仅须要在任务中拥有接收者的弱引用,在回调到master线程时检測一次。





1.8最后须要解决的问题

设计master-worker的问题终于转换为:任务队列设计。向指定线程派发任务时仅仅须要向相应worker

的任务队列加任务。master线程上拥有的任务队列用windows消息来实现(通常master线程是主线程,

拥有消息循环)。worker线程上的任务队列就依据须要实现了。

2.任务队列设计

如前所述,该任务队列仅仅在单独线程中訪问(指仅仅有一个线程取任务,而派发任务可能是多线程的)。

由于明白了是由派发器向谁派发任务(派发器向不明白的多个线程派发任务的模型不考虑)。

在服务对象内部,经常是在初始化时开线程。线程接收服务对象传来的任务,没有任务时就等待。

通常仅仅开一个线程。

而对于任务派发器,可能会开多个线程。甚至有可能在收到任务时继续开线程。

在这里仅仅讨论开一个线程,接受任务的情况。





一个基于线程消息的工作线程例如以下:





std::vector<Task> task_list;





while (GetMessage())

{

TranslateMessage();

DispatchMessage();



switch (msg.message)

{

case WM_ADD_TASK: task_list.push_back(msg.wParam); break;

case WM_DEL_TASK: 删除任务逻辑; break;

}



while (task_list.size())

{

if (PeekMessage()) break;



从task_list中取出一个任务。

运行任务。

}

}





在这里。任务通过消息的wParam和lParam来传递。当然,在消息没有被收到的情况下。会造成内存

泄漏。

在任务运行过程中,是删除不了的,这须要加额外逻辑。





一方面须要添加删除标记。使得任务完毕时不回调。

此外有可能在任务已经完毕的时候。正在回调

的过程中中止任务。这须要在master线程中再检測一次标记。





还有一方面假设要保证任务即时退出,须要任务带一个StopEvent。在耗时处轮询这个StopEvent。

外部停止任务时,须要激发StopEvent。

另外,任务异常会影响整个任务队列。有必要的话,自己try catch一把。

到如今为止,任务队列没有锁,或者说被一些windows的机制掩盖了锁的存在。













再来个任务队列:





HANDLE events[] = {停止事件。有任务事件};

for (;;)

{

if (停止事件发生) break;



Task task;

if (!LoadTask(&task))

{

等待停止事件和有任务事件。

continue;

}



do task;

}





在这里任务被放在一个队列中,对队列的訪问应该是相互排斥的。









任务队列还有非常多变形:单个任务的任务队列,后面的任务能够把前面的任务替换。

任务

带上优先级。





3.chromium的线程机制

參与者:

Thread:

线程对象的封装.

在运行时会在内部使用MessageLoop进入相应的循环.

对外提供mesage_loop_proxy使得能够往该线程上派发任务.

对外提供接口訪问关联的MessageLoop的指针,弱.



MessageLoop:

消息循环.

聚合MessagePump进行消息循环.

维护task队列,在响应pump的回调时处理任务队列.

构造时将自己写到tls中,向外提供静态方法获取当前纯种的MessageLoop.

结束时剩余任务不作处理.



该类提供的派发任务接口仅仅建议在当前线程使用.





MessagePump:

消息泵.

负责起消息循环,调度MessageLoop.



MessageLoopProxy:

消息循环代理.

和某个MessageLoop绑定,提供向该MessageLoop派发任务的接口.

提供接口获取当前线程上的MessageLoop的MessageLoopProxy.



通过代理能够在随意线程上向指定线程派发任务,能够把代理传到某个对象中保存起来,

然后调度时就向固定的线程派发任务,而不必知道详细是哪个线程.

代理指向MessageLoop内的MessageLoopProxy,通过引用计数维护生命周期.

代理向MessageLoop派发任务时会保证MessageLoop一定存在,或者检測到MessageLoop已析构时返回失败.





BrowserThread:

线程池。维护固定类型的线程。

提供向详细线程派发任务,获取指定线程上的message_loop_proxy等接口.



派发任务时通过原子訪问全局对象中注冊的线程将任务派发到指定线程.

提供的MessageLoopProxy的实现仅仅保存相应的线程ID,在派发任务时调用PlayerThread的派发接口.

和前面的代理类相比,不參与MessageLoop内部的代理类的生命周期管理,可是和须要原子地訪问全局对象.

在訪问全局对象时通过一定的手段,在某些情况下做到无锁訪问.





Chromium的线程主要是指这里的通过固定类型可以訪问的线程。(通过Pool维护无固定类型的线程并提

供相应的调度接口暂不讨论)





能够看出,调度接口分三个级别:

线程池级别,代理级别,详细线程(消息循环)

依据自己须要选择不同级别的接口.不同的级别有不同的安全性保证和需求.





为了支持任务。还引入了Bind机制:





Bind:

支持普通函数和成员函数,使用成员函数时第一个參数为相应的对象指定,且要求对象是ref counted的.



Bind后返回Callback对象,假设一个Callback对象的返回类型是void且參数类型也是void,则称这个Callback为Closure.

在线程池处理的任务均为Closure.



在Callback内部将函数指针,參数所有保存起来,然后在调用时将參数应用到函数指针上.

因此,如此參数是很引用,表达出直接改动某个对象的语义时和Bind的实现相悖,不支持.通过静态断言阻止这样的做法.



成员函数调用时,在保存參数时会添加对象的引用计数,调用完毕后降低引用计数.



对于scoped_ptr,scoped_array,scoped_ptr_malloc,ScopedVector类型的參数,会通过构造的move语义支持进行优化.



因此建议线程池上的任务,要少引用外部的东西,比方绑定到成员函数则将相应对象引用计数加1,又如參数为智能指针

时会将智能指针的引用计数加1.这样,任务没有运行而应用程序退出时,会在线程池的反初始化时再释放这些对象,

可能存在问题.所以,任务对外部依赖要足够少.



以下是一个做到外部依赖足够少的參考解决方式:

比方A类要干某件事,能够考虑建一个B类,B类负责干这件事,存储一些数据,同一时候有一个指针指向A.A须要干事的时候

就new一个B类的对象,并保存在A的智能指针成员中.A类对象析构时,调用B类的接口告诉B,A已经析构了.B类完毕任务

时,检查一次A是否析构,假设析构则什么也不做(假设通过成员变量标记,本次訪问该标记时不太靠谱,因此该线程在

读,还有一个线程可能在写.这里訪问仅仅起优化).B类回到A调用B的线程时,再一次检查A是否析构,假设析构则什么也不

做,否则能够处理业务逻辑.这一次对标记的检查会是安全的,因此A也仅仅在这个线程中保证写标记.



因为B类的独立性高,所以easy做到能够在随意时刻析构.





Chromium的线程池分三类:

Default仅仅负责处理任务队列。

UI会起消息循环,同一时候处理任务队列。

IO可以检測到IO的完毕或JOB的信息。总之,能挂在完毕port上的就能检查,同一时候处理任务队列。

事实上现难度主要是在当中的pump上:





Default的pump:

有Work时保证有再Work一次的机会。

有DelayedWork时保证Work和DelayWork都有再处理的机会。

否则,觉得能够处理IdleWork。

假设没IdleTask,依据一定策略计算等待时间,等待到新任务或有DelayTask处理。

MessageLoop在加任务时运行ScheduleTask,唤起可能的等待。



UI的pump:

假设仅通过派发线程消息来通知任务,则会存在问题:UI消息循环中处理消息时再起消息循环,使得

无法收到派发给该线程的消息。

所以。须要在线程上建窗体来处理消息。



一个简单的实现就是每加一个任务则Post一个任务消息到这个窗体,可是chrome在此基础上作了优化:

保证消息队列中至多仅仅有一个任务消息。



首先用一个变量have_work_表示有任务消息待处理的状态。为1时表示有任务消息须要处理,0表示

没有任务消息须要处理。而该变量的维护通过原子操作实现,有资料表明比临界区快一倍。发任务

消息时。假设发现have_work_为1。则什么都不做。

消息循环从消息队列中取出任务消息时。则将

have_work_置为0。



然而,这里存在竞争关系,当消息循环从消息队列中取出消息时,have_work_没有被设置为0。

在此

同一时候加入任务,发现have_work_还是1。于是就不发消息。假设在have_work_被设置为0后,进入空暇

状态。则有消息丢失。



所以在通过窗体过程中确定处理一个任务后还要额外再ScheduleTask一把。

因为这里会自己处理消息。所以还有所优化。

在有任务消息的时候,不须要派发到窗体过程。而是在

自己的循环中一方面处理一条非任务消息。然后调度一次任务。交替进行。所以上述的窗体过程中处理

消息普通情况下调用不到。



在处理消息的时候有两种情况,一种是任务消息,一种是其他消息。在处理任务消息时,和要处理非

任务消息的设计不合,所以还会从消息队列里再拿一次消息。而对于其他消息,则处理一次,返回true。



在消息处理函数后紧接着就是任务队列的处理,两者不能交换。假设在处理完work后。其他线程加一

个任务进来,这时消息处理函数发现仅仅有一个任务消息,于是返回false。可能终于陷入等待消息的状态。

使得这个消息没有即时处理。



在没有消息,且没有Work(含Delay)须要处理时。觉得能够处理一下IdleWork。



IO的pump:

当有Work时使得Work另一次运行的机会。

有不论什么完毕事件时,又使得Work有运行的机会,且完毕port有检測的机会。

有DelayWork的时候,前面两者和DelayWork本身有处理的机会。

否则。处理IdleWork和等待,等待时依据策略计算等待时间。





外部通过向完毕port发消息唤醒可能的等待。

IO的pump相同保证至多有一个task的完毕事件在队列中。

假设事件被取出,且标志没有被置为0,没有关系。

由于不论什么完毕事件都使得Work有运行的机会。

版权声明:本文博主原创文章,博客,未经同意不得转载。