Go与并发模型
来源:互联网 发布:安卓手机优化 编辑:程序博客网 时间:2024/06/06 03:52
最近,Google的Golang荣获2016年度编程语言,作为一个谷粉,我理应写一篇文章以表祝贺,正好碰上最近在弄一个支持IO无锁并发的脚本引擎,参考了一些Go内部的设计思想,真想不到Go的萌萌的外表下竟隐藏着一颗如此强大的内心,本文主要的分析焦点就是做到更干净的并发编程,让并发编程的代码更加优雅~
并发编程 & Coroutine
首先,我们要写一个并发多任务的程序,首先想到多线程。。。不妨假设一个简单的例子,生产者-消费者模型,生产者负责生成资源,消费者消耗之,一般来说,消费者的处理速度是远不及生产者的产生速度,比如高并发的服务中的MQ
,在单进程的环境下可以考虑两个线程完成这个模型,一个线程负责生成数据(客户端的request
),一个线程负责处理这些request
,显然速度的差异带来的结果就是需要一个异步队列来存放请求,真正的MQ
可能会有很多个生产者,也有很多个消费者,我用python来模拟一下上述的情况,
def produce(): print('produce loop thread start') while True: while len(asyncQueue) < threshold: try: lock.acquire() asyncQueue.append('task') print('add a request to queue') finally: lock.release()def consumer(): print('consumer loop thread start') # time.sleep(600) counter = 0 while True: time.sleep(6) if not len(asyncQueue): counter = counter + 1 if counter >= 3: return continue try: lock.acquire() task = asyncQueue[0] print('consuming queue, current size of queue is %d' % len(asyncQueue)) callabck(task) time.sleep(5) asyncQueue.pop(0) finally: lock.release()
可以把这些实现放到两个线程中跑,通过多线程的配合,可以很好完成我们的并发任务,然而,这种方式的代价是同步问题,需要互斥锁,锁这种东西是很容易就出现dead lock
这类很尴尬的问题的,而且,线程的切换对于操作系统来说可不是一件小事,虽然当前的page
还在内存里,但这种调度发生在内核态,保存原线程的堆栈进stack segment
,加载目标线程的堆栈进寄存器,最终跳到它的入口地址,在多线程并发的环境下,用户态会和内核态频繁切换,如果没有内存映射措施对的话,就会不断发生数据的拷贝,这是很消耗CPU资源的一件事,所以在高并发下,多线程比较简单粗暴,但并不是一种很优雅的实现。
那么怎样才比较fashion呢,首先,考虑无锁,其次,如果我们把内核态的调度转移到用户调度,把线程这个概念再抽象一层,我们自己来实现线程,自己来完成线程的调度,这种自己定义的线程完全可以按照自己的需要进行设计,调度,共同协作完成任务,考虑使用协程(Coroutine),我现在用协程的概念再用单线程无锁的方式实现一次生产者-消费者模型,python没有协程的工具包,但可以用生成器(generator)来代替,
def produce(): r = '' print('switch to await') while True: res = yield r # print type(r) if not res: continue print('get result ==> %d' % res) r = 'finish'def consumer(gen): # initialize generator gen.send(None) n = -1; while n < 5: n = n + 1 # switch to produce for resource print('producting resource %s...' % n) r = gen.send(n) print('consuming resource %s...' % r) gen.close()gen = produce()consumer(gen)
其实基本的概念很简单,假设有两个任务Y1,Y2,Y1执行到一段时间发现它需要依赖另一个任务Y2的结果才能执行下去,于是它就很慷慨的让出CPU时间,对应于上面代码的yield
于是调度器获得执行,然后决定执行Y2,等到Y2产生Y1等待的结果后,Y2再将控制权交回到调度器,调度器再次唤醒Y1,让它接受Y2传递的结果从上次暂停的地方继续执行,这就是基本的工作流程,直到所有的任务都被协作完成,整个流程无需多线程,无锁,代码实现非常干净~
Container Thread
如果要实现一个高并发的IO模型,如何发挥协程的优势呢,首先来看看Node怎么搞的,Node的威力依赖于OS的AIO,网络的IO完全是可以真正意义异步的,然而大量数据库的磁盘IO无能为力,还是依赖底层的线程池分配一个线程阻塞等待(休眠),不能再休眠时间干别的事情,涉及线程池,上面说的切换代价就必不可少了,无独有偶,我在设计引擎的并发模块时参考了Go的内部设计,发现它几乎解决了线程池并发以及协程的一切痛点,于是我选择了一种新的并发模型,将OS内核态的工作自己在用户态手动完成,首先系统真正意义上的线程即native thread
我称为一个container
即上面运行很多更小的运行单元(线程相当于原子,协程单元类比为夸克),她们由协程的调度器进行调度,处于用户态,基本的架构如下
协程单元(之后简称为cor)可以先考虑它会在两个状态之间切换,当然实现起来会有两外的一个初始态和一个调度插入态,当cor遇到yield后,就会保存当前的运行堆栈,跳到调度器的代码段,进入Await
状态,等待调度器下一次带着结果将它唤醒,这部分协程级的调度我用下面的代码模拟,
//similar to NThreadScheduler//In this case, asynchronous task such as NetworkIO which is handled via AIO is handled by coroutine instead of NThreads, which scheduled by system suitable for muti-processers//but some IO for Disk like database depends on NThread Scheduler to give up and find other hostsvoid CorScheduler::Dispatch(msg_t msg) { //M:N dispatcher if (priority_cors.IsEmpty()) return; switch (msg.type) { case msg_t::kCorEnded: //a cor yields case msg_t::kCorsAwait: __msg_type__ cor_id = msg.content; //take out the one who has largest priority //compute via 'M = Y * A', in which 'M' is for priority vector, 'Y' is matrix for resource occupied by cors //and A is a weight vector, this list is sorted by priority //FOREACH_EXCEPT_AWAIT(except, set, operation:Lamda expression) //when 'Await', continue FOREACH_EXCEPT_AWAIT(cor_id, priority_cors, [](const CoroutineUnit& cor) { //cors interact with 'args' if (cor.status == CoroutineUnit::kPreparing) cor.Start(); else { cor.Resume(ref_running.args); } }); break; //for preemption case msg_t::kCorsInterrupt: //compute cpu usage info and IO time //predicted by last IO time auto status = ref_running.status; _assert(status == CoroutineUnit::kRunning || status == CoroutineUnit::kPreparing); auto res_info = AnalysisYRes(msg.content); auto winner = OptimizeRes(ref_running.id, msg.content); CoroutineUnit& intr_cor = *static_cast<CoroutineUnit*>(msg.data); if (winner.id == ref_running.id) return; //current cor -> Await state auto args = ref_running.Yield(); intr_cor.SetArgsForRes(args); //only preparing cor can interrupt a running cor _assert(intr_cor.status == CoroutineUnit::kPreparing); intr_cor.Start(); break; //when looper notify scheduler that asynchronous tasks are fullfilled case msg_t::kCorsAsyncFullfilled: CoroutineUnit& yielded_cor = *static_cast<CoroutineUnit*>(msg.data); _assert(yielded_cor.status == CoroutineUnit::kAwait); auto args_map = SearchForArgs(); if (ref_running.status == CoroutineUnit::kDying) { yielded_cor.Resume(args_map); } else { yielded_cor.Interrupt(args_map); } break; }}
在这个调度器中我设计了一个优先规则,定义一个资源分配矩阵Y,元素为Yst,为cor[s]在t时刻占有资源R[t]的量,定义一个资源权重矩阵A,优先级向量M = Y * A
权重可以根据当前性能限制设定,比如主DBIO的情况下,IO时间短的具有优先级,可以插入长IO操作,提升CPU利用率,堆上分配的内存也可以量化进Y矩阵,代码中所有关于优先级的分析都是基于Y矩阵分析,上面还有一个状态专门描述从AIO尤其是网络IO是不会阻塞当前线程,所以交给当前的cor执行就可以,只有阻塞了当前线程,线程级的调度器才会考虑解决办法。
状态的切换的描述也是比较简单的
void* CoroutineUnit::Yield() { StoreContext(); this->status = kAwait; //pass its args to all cors auto res = SpreadArgsAll(this->scheduler->priority_cors, this->arg_expr); return res;}void CoroutineUnit::Resume(__args_list_type__& args) { RetrieveContext(); this->status = kRunning; SetArgsForRes(args);}
假设我们有N个协程,又一个进入等待状态,那么如果它需要向它的合作者传递一个信息,嘿,我目前的进展是这个,你接着我这个继续往下干呗,但这样很难知道究竟要向哪一个目标传达,为了简单粗暴(而且由于线程级调度的存在,容器跟cor的耦合度不能太高,cor集合是可能变化的),直接就让它对着N - 1个协程发个广播消息,你们听好啦,我现在干到这一步,如果你们中谁有幸接替了我的工作,就根据这个信息继续干,我睡觉去了哈~
狂热的偷取
现在问题来了,如果当前的cor阻塞了整个线程,线程级调度该怎么解决,要知道,还有很多cor基于这个container
等着执行,所以容器不行了,第一件事就是及时切换容器,我们需要分析当前线程的负载状况,找出一个比较闲的线程(如果没有线程,就分配一个新的),将剩余的cor装进去,丢下正在pending状态的cor以及它的NThread
,然后过了那么一段时间,这个被遗忘的cor终于从漫长的阻塞中返回了,然而它却处于一个空负载的状态,所以它需要。。去偷cor,先去看看其它的线程有没有机会下手,比如看看哪个线程负载特别繁重,那就偷它的,或者大家都很清闲,那就偷全局的cor队列,这个全局队列都是由一些孤儿组成,当孤儿cor寻找宿主时会通过Y矩阵计算优先级向量,将一些优先级低的cor暂时放在全局队列,等待复苏的线程进行接管,由于这个时间延迟不确定,所以只能选择优先级低的cor,再来模拟一下线程级的调度
void NThreadScheduler::Dispatch(msg_t msg) { if (this->pool.size() < kNThreadPoolSize) { CreateNewNThread(kNThreadPoolSize - this->pool.size()); } switch (msg) { case msg_t::kNThreadsBlock: //we will find another idle thread NThread t_idle = this->pool.Retrieve(); NThread t_block = this->pool.Get(msg.content); TransferContext(t_block, t_idle); break; case msg_t::kNThreadsLoopSysCallReturn: //search for globalRunQueue and steal from others NThread t_retrieved = this->pool.Get(msg.content); auto target = AnalysisUnbalance(); if (target) { t_retrieved.appendCors(StealCors(target.president->cors, Common::Fix(target.president->cors.size() / 3))); return; } if (!globalRunQueue.IsEmpty()) { //just comsume 1/6 of this queue, for balance size_t ns = Common::Fix(globalRunQueue.size() / 6); t_retrieved.appendCors(StealCors(globalRunQueue, ns)); } break; case msg_t::kNThreadsRun: __assert(this->pool.IsInitialized()); this->pool.StartAllTask(); }}
可以看到状态kNThreadsBlock
跟kNThreadsLoopSysCallReturn
描述了线程进入阻塞以及复苏,基本跟上面描述的过程一致,不过我添加了一些控制负载均衡的系数,其实这里的NThread
还是维护在一个池对象里面,不过用户级控制的比重大大增加,而且可以看到,通过协程,现在我们定义的线程处理多任务的能力是大大提升了,传统线程池线程阻塞后基本它之后的代码无法执行,然而现在线程执行的任务被拆分成很多个cor,阻塞只会影响其中一个cor,其它属于这个线程的代码会以其它cor的形式转移给其它线程,针对现在多核架构这个利用率将是很高的。
转移线程的过程也是一个偷取过程,目标线程将阻塞线程剩余的cor都偷去过来,然后跟自己的优先级进行优先级向量的比较,进行适当的cor的优先调度,这样CPU将会很高效地帮我们干活~
void NThreadScheduler::TransferContext(NThread& from, NThread& dest) { auto new_cors = StealCors(from.president->cors, from.president->cors.size() - 1); dest.appendCors(new_cors); //remove all the idle cors from.ClearIdle(); //interrupt the current and adjust the queue, maybe some cors will be pushed to the global queue auto optimal_cor = CorScheduler::SelectOptimalCor(new_cors); dest.president->Dispatch({ msg_t::kCorsInterrupt, optimal_cor.id, &optimal_cor });}
OK,这种并发模型的核心部分原理基本就是这样,威力是非常强大的,看看Go,就知道这东西有多厉害~~
One Loop per Thread ?
当然还有一些网络IO库采用这种并发模型,其实大致的思想是类似的,都是想让线程发挥更大的效能,当然,我在自己的轮子中还是可以继续借鉴一下这种方式的,目前在设计脚本的并发模块yarm,之后再来给大家分享网络模块pink,以及VM Zygotor,代码之旅,永不停息~
- Go与并发模型
- Go并发模型学习
- go-并发与并行
- Go方法与并发
- Go语言并发模型:使用 context
- 再谈CERL:详论Go与Erlang的并发编程模型差异
- 并发并行与Go并发编程
- ngx_lua与go高并发性能对比
- Go 1.5的并发特性与案例
- ngx_lua与go高并发性能对比
- Go 语言的并发模型--通过通信来共享内存
- Go并发模型之生动我见(一)
- Go 并发
- go并发
- go-并发
- Go并发
- Hive锁与并发模型
- nginx并发模型与traffic_server并发模型简单比较
- uiscrollview 怎样去掉导航栏对他顶部的空白的影响
- ACM篇:UVa 839 -- Not so Mobile
- windows7 在cmd中执行php脚本
- Spring Security-- 验证码功能的实现
- linux服务器rm删除文件后磁盘空间没释放
- Go与并发模型
- MySQLCluster数据节点内存空间释放
- 紫书 例题 10-7 UVa10820 欧拉函数
- oc-Foundation_02_NSArray
- java POI创建Excel的单元格并填值
- Android自定义状态栏颜色与应用标题栏颜色一致
- 十进制转二进制(easy)
- mysql获取时间
- 两只老虎改编(天线宝宝版)