ISO/IEC C++ China Unofficial

 找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索
查看: 369|回复: 1

[C氵] 非抢占式并发(2):p7的调度概况

[复制链接]

3

主题

20

帖子

202

积分

超级版主

mikonmikonmi

Rank: 8Rank: 8

威望
4
经验
170
贡献
4
发表于 2016-1-10 15:56:33 | 显示全部楼层 |阅读模式
窃以为比起单纯介绍不那么好看的接口,不如指出p7到底做了什么来得爽快。所以这帖就简单介绍一下p7的调度循环。

题外话。有些人喜欢谈论开闭原则,也即所谓的“对扩展开放、对修改关闭”。然而这个原则一旦被见识短浅或是心胸狭窄的库设计者投入应用,则会产生糟糕的后果:库极力隐藏“底层”机制,使得库的接口不再与OS提供的本地API通用,从而库的用户被迫使用库提供的接口,以至于用户的创造力被库作者贫瘠的想象力制约。如果库的设计者头脑清醒,这可能还不是件坏事,因为总归可以节省体力、提高信任;可惜不是所有的库作者都能达到这样的程度,于是人间惨剧一幕一幕上演,横竖都拦不住。欲为人师者,还望再思三思。

回到正题。p7给出的coroutines, 实际上已经是userspace threads. 它们被运行调度循环的pthread执行。这些pthreads称为carriers, 它们持有三种队列:
  • 调度队列。这队列中包含了可调度的coros. 即使它们曾经主动放弃执行,也具有被选中执行的权利。
  • 阻塞队列。这队列中包含了因为某些原因(竞争锁失败、等待内部通信、等待另一个协程执行结束)而暂时不可继续被执行的coros.
  • 请求队列。这队列中包含了来自其他carriers的coro创建请求。在合适的时机,请求所包含的函数和启动参数将被真实地创建在本carrier上。
某些语言的用户态并发实现允许work stealing. 例如若global queue中没有coros可以调度,则从某个其他调度器的队列中取得一半coros占为己有。对具有独立可靠运行时的语言来说,这是颇理想的实现。但p7工作的环境中可能有用户指定的TLS, 而若入口函数在使用TLS, 则这一假设不可以被破坏。为保证TLS的一致性,p7的负载均衡在coro创建请求发生时一次性完成,此后不再迁移coros.
作为回报,p7的调度队列和阻塞队列是不锁不等的。它们只被本carrier操作。请求队列在请求方试图投入请求-应答方试图应允请求时,应答方是不锁不等的。这个回报并不值得期待。实际上,如果单纯运用p7的话,也不该假设原生线程或pthread的TLS, 因为这一细节不应被协程所知。这里我的考虑并非出于库本身在模型上的完整性,而是出于对屑现实的妥协。如果我不妥协,那么这库会逐渐把自己封闭起来——有如可怜清高的上大人孔已己。


目前(2016.1.10)的调度循环,按顺序执行下列工作:
  • 检查调度队列和请求队列是否全部为空。若空,则初步认定epoll_wait可以阻塞。此处有请求队列上的互斥。
  • 检查最早的超时定时器是否已经超时。目前的超时定时器队列是一棵红黑树。
  • 综合上述两个结论,决定epoll_wait的超时值:到最早的超时定时器为止、永远,或立刻返回。
  • 对本carrier的epoll fd进行epoll_wait. 等待的集合里,包括所有用户指定的按fd等待的IO, 以及内部通信管道(不阻塞不ET)。在进入epoll_wait前,原子地保证其他carrier可以了解本carrier进入了epoll_wait. 返回后,原子地保证其他carrier可以了解本carrier已经从等待中返回。
  • 按超时时刻从早到晚检查超时定时器,直到当前检查的定时器没有超时。如果定时器有回调,就执行回调;如果定时器和一个协程关联,则设置该协程的超时状态。
  • 对epoll汇报的就绪fd集合进行处理。若fd不是内部管道读端,则它对应一个IO停止续延,这续延被创建在发起IO之coro的栈上,保存了必要的fd和coro信息。就绪的IO将取消coro的超时状态,因而需要超时定时器完成特定任务的coro不应手工检测这一状态:它们应当依赖自身注册的回调完成任务。若fd是内部管道读端,其中能原子地读出至少一份完整的内部通信报文。调度循环矢量地读出报文,并用报文的类型回调对应的处理函数。目前的处理函数有二:coro创建唤醒和coro内部通信抵达。同样,coro内部通信抵达的处理函数将会抵消内部超时状态。这一抵消的合理性,在于coro“一时刻只能做一件事”的基本事实。有关内部通信,我不打算在这帖里详述,留到下一帖吧。
  • 交换请求队列的双缓冲,不锁不等地应允所有coro创建请求,将它们变成本carrier上可调度的coros.
  • 分发coro内部通信。简略地说,coro对coro的通信将会被转发到carrier上,由carrier在调度时投递到确定的目标上。具体的细节同样留到下一帖(实际上这里或许需要更多的检验,目前是个小坑。Actor concurrency目前仍然正在调整中。)
  • 完成重调度。若前一个被调度运行的coro仍然有权利被调度,那么将它轮转到队尾。不论是否如此做,调度队列里的下一个coro.

p7会征用调用了p7_init的pthread, 将其控制流转移到调度循环上。这并不是很好的实践,只是我懒得改了。如果用户指定了多于一个pthread执行协程,则会创建出多余的carriers, 以调度循环为入口。

实际上p7的调度循环——如同读者熟悉的那样——是一个单纯的reactor. 我曾经试过将IO就绪与调度分开的实现,不过最后选择了这种综合结构的原因还是对既定TLS的妥协。当然这是值得批判地考虑的一点,并非完全的上策。

调度循环的代码在 这函数 当中。读者现在应该能够粗略地将之前的条款对应上实现了。
有关围绕着内部通信管道的事情,就留到下一个坑吧

评分

参与人数 1威望 +1 贡献 +1 收起 理由
LH_Mouse + 1 + 1 (。・ω・)ノ

查看全部评分

沿海征收头GAY骨
回复

使用道具 举报

3

主题

74

帖子

283

积分

中级会员

Rank: 3Rank: 3

威望
9
经验
182
贡献
9
发表于 2016-1-11 23:13:07 | 显示全部楼层
囧,完全看不懂_(:3」∠)_
#if !idppc
/*
** float q_rsqrt( float number )
*/
float Q_rsqrt( float number )
{
        long i;
        float x2
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Archiver|ISO/IEC C++ China Unofficial

GMT+8, 2017-12-18 09:28 , Processed in 0.054814 second(s), 23 queries , XCache On.

Powered by Discuz! X3.4

© 2001-2017 Comsenz Inc.

快速回复 返回顶部 返回列表