erlang send_after 源码剖析

概念简介:
tick最小单位为1毫秒
每个调度进程有自己的时间轮–(soon轮 later轮)

soon轮 早轮
早轮的每个槽宽度为1个tick,一共有2的14次方个槽。存储大约16秒内需要被触发的定时器

later轮 晚轮
晚轮的每个槽宽度为2的13次方个tick,一共有2的14次方个槽。存储大约37个小时内需要被触发的定时器

超过晚轮的定时器,怎么处理?
超过晚轮的定时器,暂且称为高级定时器; 没有超过晚轮的定时器,暂且称为普通定时器。高级定时器信息存于ErtsSchedulerData 的timer_service指向的结构中, 所有的高级定时器会计算一个最近的到期时间,构造成一个定时器插入到时间轮中,这个构造的定时器暂且称为高级总管定时器

整个时间轮流程文字叙述:

1:插入小于等于当前时间的定时器,定时器将被安插到时间轮的AtOnce插槽,erts_bump_timers被触发时立刻触发定时器
2:插入小于大于2的14次方毫秒的定时器,定时器将被安插到早轮的插槽中,早轮插槽一共2的14次方个,每个插槽存储1毫秒内触发的定时器,早轮插槽中的定时器以双向链表的数据结构组织,插入时间复杂度为O(1);
3:插入大于2的14次方毫秒,小于2的(13+14)次方毫秒的定时器,定时器将被安插到晚轮的插槽中,晚轮插槽一共2的14次方个,每个插槽存储2的13次方毫秒内触发的定时器。晚轮插槽会提前触发,触发时会将插槽内的定时器安插到早轮中;
4:大于2的27次方毫秒的定时器(称为高级定时器),将以红黑色的形式被保存在ErtsSchedulerData中,然后计算出最近的触发时间,以该触发时间设置一个定时器(为方便后文区分,这个定时器暂且称为高级总管定时器),插入到晚轮中。

以下为实现流程分析:

结构简介:
调度器结构 ErtsSchedulerData_ 其中三个字段与时间轮有关:

ErtsTimerWheel *timer_wheel;   指向时间轮的指针
ErtsNextTimeoutRef next_tmo_ref;    时间轮结构中next_timeout_time的引用
ErtsHLTimerService *timer_service;  高级定时器服务结构 存储处理高级定时器需要的相关信息

高级定时器服务结构 ErtsHLTimerService

ErtsHLTCncldTmrQ canceled_queue;
ErtsHLTimer *time_tree;          存储所有的高级定时器
ErtsBifTimer *btm_tree;          存储了普通定时器和高级总管定时器
ErtsHLTimer *next_timeout;       所有高级定时器中,最近的高级定时器
ErtsYieldingTimeoutState yield;  中断标记,类似于reduction机制,可先忽略
ErtsTWheelTimer service_timer;   存储高级总管定时器

时间轮结构 ErtsTimerWheel

ErtsTWheelTimer *slots[1                         立刻触发插槽 
    + ERTS_TW_SOON_WHEEL_SIZE                    早轮  ERTS_TW_SOON_WHEEL_SIZE 一般为214次方
    + ERTS_TW_LATER_WHEEL_SIZE];                 晚轮  ERTS_TW_LATER_WHEEL_SIZE 一般为214次方
ErtsTWheelTimer **w;			        w指向slots[1]的地址
Sint scnt[ERTS_TW_SCNT_SIZE]; 			效率优化字段,记录每512个插槽内定时器的数量
Sint bump_scnt[ERTS_TW_SCNT_SIZE];
ErtsMonotonicTime pos;				时间轮处理到的时间
Uint nto;					定时器个数
struct {
Uint nto;					需要立刻触发的定时器个数
} at_once;							
struct {
    ErtsMonotonicTime min_tpos;		早轮中最近触发定时器位置
    Uint nto;					早轮中定时器个数
} soon;
struct {
    ErtsMonotonicTime min_tpos;		晚轮中最近触发定时器位置
    int min_tpos_slot;				晚轮中最近触发插槽
    ErtsMonotonicTime pos;			晚轮处理到的位置 按照晚轮的宽度进行增长
    Uint nto;					晚轮中定时器个数
} later;
int yield_slot;				中断插槽----时间轮处理有类似reduction的机制,处理了一定数量的定时器后会触发中断
int yield_slots_left;
ErtsTWheelTimer sentinel;			哨兵,用于遍历时间轮
int true_next_timeout_time;
ErtsMonotonicTime next_timeout_pos;		下次触发时间轮位置
ErtsMonotonicTime next_timeout_time;		下次触发时间轮时间

时间轮定时器结构 ErtsTWheelTimer

ErtsMonotonicTime timeout_pos;        定时器到期时间 
struct ErtsTWheelTimer* next;         下个定时器
struct ErtsTWheelTimer* prev;	      前一个定时器
void (*timeout)(void*);   	定时器到期触发函数   高级定时器触发函数为hlt_service_timeout; 早晚轮触函数为tw_bif_timer_timeout
void* arg;              定时器到期触发函数的参数
int slot;

Bif定时器结构 ErtsBifTimer,描述通过bif创建的定时器 eg:send_after

union {
    ErtsTmrHead head;    高级定时器和普通定时器通用的一些属性
    ErtsHLTimer hlt;     高级定时器
    ErtsTWTimer twt;     普通定时器
} type;
struct {
    erts_atomic32_t state;
    Uint32 refn[ERTS_REF_NUMBERS];     定时器ref  eg:send_after的返回值
    ErtsBifTimerTree proc_tree;
    ErtsBifTimerTree tree;
    Eterm message;                    定时器信息  eg:send_after的第三个参数
    ErlHeapFragment *bp;              定时器信息存储的位置
} btm;

ErtsTmrHead 该结构不是重点

Uint32 roflgs;		定时器的一些标记,eg:标记send_after的第二个参数是pid还是regname
erts_atomic32_t refc;   该结构的引用计数,eg:send_after的第二个参数是pid时,该值为2,因为pid的进程结构bif_timers字段也指向了当前结构

普通定时器ErtsTWTimer

ErtsTmrHead head; /*ErtsTWTimer的首位必须存放ErtsTmrHead字段*/  由于ErtsTWTimer存放在ErtsBifTimer的type字段里,union格式的需要
union {
    ErtsTWheelTimer tw_tmr;
    ErtsThrPrgrLaterOp cleanup;
} u;

高级定时器ErtsHLTimer

ErtsTmrHead head; /* NEED to be first! */  原因同上
ErtsMonotonicTime timeout;    超时时间
union {
    ErtsThrPrgrLaterOp cleanup;
    ErtsHLTimerTimeTree tree;    同一超时时间超时的高级定时器,以链表的形式存于此结构中
} time;

函数实现:
create_tw_timer函数插入非高级定时器核心逻辑:

static ErtsTimer *
create_tw_timer(ErtsSchedulerData *esdp,...)
{
    tmr = (ErtsTWTimer *) erts_alloc(ERTS_ALC_T_BIF_TIMER, sizeof(ErtsBifTimer));  注:tmr指针类型是ErtsTWTimer,但是是按照ErtsBifTimer的大小申请的空间

    timeout_func = tw_bif_timer_timeout;  定时器到期时触发函数
    refc += init_btm_specifics(esdp,(ErtsBifTimer *) tmr,msg,refn);  主要逻辑将ErtsBifTimer->btm.message和refn(具体含义见上文)赋值到tmr中
    将定时器插入时间轮的核心逻辑
    erts_twheel_set_timer(esdp->timer_wheel,    时间轮结构   
			  &tmr->u.tw_tmr,   ErtsTWheelTimer定时器结构   将此结构插入时间轮中
			  timeout_func,     定时器到期时触发函数
			  tmr,              定时器到期时触发函数的参数  即到期时会触发 tw_bif_timer_timeout(tmr)
			  timeout_pos);     定时器到期位置
    return (ErtsTimer *) tmr;
}

create_hl_timer函数插入高级定时器核心逻辑:

static ErtsTimer *
create_hl_timer(ErtsSchedulerData *esdp, ...)
{
    tmr = (ErtsHLTimer *) erts_alloc(ERTS_ALC_T_BIF_TIMER, sizeof(ErtsBifTimer));   给tmr申请空间
    refc += init_btm_specifics(esdp,(ErtsBifTimer *) tmr,msg,refn);   同上

    if (!srv->next_timeout || tmr->timeout < srv->next_timeout->timeout) {  若高级总管定时器为空 或者 当前高级定时器出发时间小于高级总管定时器的触发时间
        if (srv->next_timeout)
            erts_twheel_cancel_timer(esdp->timer_wheel,&srv->service_timer);  则取消旧的高级总管定时器
        erts_twheel_set_timer(esdp->timer_wheel,    向时间轮中插入新的高级总管定时器
            &srv->service_timer,                   将高级总管定时器结构插入时间轮
            hlt_service_timeout,                   高级总管定时器到期时触发的函数
            (void *) esdp,                         高级总管定时器到期时触发函数的参数  即到期时会触发 hlt_service_timeout(esdp)
            tmr->timeout);
        srv->next_timeout = tmr;
    }
    st_tmr = time_rbt_lookup_insert(&srv->time_tree, tmr);   搜索同一时间触发的高级定时器,若没有则插入到按触发时间排序的红黑树中
    if (st_tmr)
        same_time_list_insert(&st_tmr->time.tree.same_time, tmr);  若有则插入到同一时间超时的双向链表中
    return (ErtsTimer *) tmr;
}

erts_twheel_set_timer 定时器插入时间轮的核心逻辑

void
erts_twheel_set_timer(ErtsTimerWheel *tiw,...)
{
    int slot;
    p->timeout = timeout;    定时器到期触发函数赋值
    p->arg = arg;            定时器到期触发函数的参数赋值
    tiw->nto++;              时间轮中定时器数量加一
    if (timeout_pos <= tiw->pos) {   超时位置小于当前时间轮的位置,则将定时器插于ERTS_TW_SLOT_AT_ONCE插槽
        p->timeout_pos = timeout_pos = tiw->pos;
        slot = ERTS_TW_SLOT_AT_ONCE;
    }
    else if (timeout_pos < tiw->pos + ERTS_TW_SOON_WHEEL_SIZE) {  超时位置属于早轮的范围内
        p->timeout_pos = timeout_pos;
        slot = soon_slot(timeout_pos);                   计算定时器所属早轮插槽     计算方法为 timeout_pos 对214次方取模 (: A&((2<<14)-1)) =:= A%(2<<14))
        if (tiw->soon.min_tpos > timeout_pos)            更新早轮的最近触发位置
            tiw->soon.min_tpos = timeout_pos;
    }
    else {
        p->timeout_pos = timeout_pos;
        slot = later_slot(timeout_pos);                  计算定时器所属晚轮插槽     计算方法为 timeout_pos 右移13位 后 对214次方取模 再 加上 214次方
        timeout_pos &= ERTS_TW_LATER_WHEEL_POS_MASK;
        timeout_pos -= ERTS_TW_LATER_WHEEL_SLOT_SIZE;    设置时间轮下次到期位置为 当前位置减213次方。  提前触发后会把处于晚轮中的定时器插入早轮中
    }
    insert_timer_into_slot(tiw, slot, p);     定时器插入时间轮指定的插槽
    if (timeout_pos <= tiw->next_timeout_pos) {    更新下次到期位置
        tiw->true_next_timeout_time = 1;
        if (timeout_pos < tiw->next_timeout_pos) {
            tiw->next_timeout_pos = timeout_pos;
            tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(timeout_pos);
        }
    }
}

时间轮的触发函数erts_bump_timers核心逻辑(略过yield逻辑, 略过使用哨兵遍历时间轮逻辑):
每次触发前,会进行判断,只有当前时间大于等于时间轮的next_timeout_time时,才会触发erts_bump_timers逻辑

void
erts_bump_timers(ErtsTimerWheel *tiw, ErtsMonotonicTime curr_time)
{
     int slot, yield_count, slots, scnt_ix;
     ErtsMonotonicTime bump_to;
     Sint *scnt, *bump_scnt;
     scnt = &tiw->scnt[0];
     bump_scnt = &tiw->bump_scnt[0];
     bump_to = ERTS_MONOTONIC_TO_CLKTCKS(curr_time);
    tiw->next_timeout_pos = bump_to;
    tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(bump_to);
    while (1) {
        ErtsTWheelTimer *p;
        if (tiw->nto == 0) {
            empty_wheel:
                重新设定各种pos;
            return;
        }
        p = tiw->w[ERTS_TW_SLOT_AT_ONCE];
        if (p) {遍历at_once插槽里的定时器,执行 timeout_timer(p)}
        if (tiw->pos >= bump_to) {break;}
        if (tiw->nto == 0)
            goto empty_wheel;
        sys_memcpy((void *) bump_scnt, (void *) scnt, sizeof(Sint) * ERTS_TW_SCNT_SIZE);
        if (tiw->soon.min_tpos > tiw->pos) {
            该代码块为更新tiw->pos,使其跳过一些空槽
        }
        ErtsMonotonicTime tmp_slots = bump_to - tiw->pos;
        if (tmp_slots < ERTS_TW_SOON_WHEEL_SIZE)
            slots = (int) tmp_slots;    早轮需要遍历的槽数计算
        else
            slots = ERTS_TW_SOON_WHEEL_SIZE;
        slot = soon_slot(tiw->pos+1);  根据当前位置计算早轮遍历的初始槽位
        tiw->pos = bump_to;   更新时间轮的位置为当前时间位置

        tiw->next_timeout_pos = bump_to;
        tiw->next_timeout_time = ERTS_CLKTCKS_TO_MONOTONIC(bump_to);
        scnt_ix = scnt_get_ix(slot);  计算slot插槽在效率优化数组scnt的索引

        while (slots > 0) {
            p = tiw->w[slot];
            if (p) {
                遍历at_once插槽里的定时器,执行 timeout_timer(p)
                注:遍历时会拿定时器的timeout_pos和需要执行到的dump_to进行对比,若timeout_pos更大,则表示该定时器为高级总管定时器,需要重新插回时间轮
            }
            scnt_soon_wheel_next(&slot, &slots, NULL, &scnt_ix, bump_scnt);  利用scnt快速寻找下一个不为空的插槽  
                                                                             scnt数组中,统计了时间轮每512个插槽里定时器的总数
        }
        if (ERTS_TW_BUMP_LATER_WHEEL(tiw)) {  判断是否需要执行晚轮  当前位置加上 213次方 大于等于 晚轮的当前位置
            restart_yielded_later_slot:
                if (bump_later_wheel(tiw, &yield_count))  晚轮的触发函数   ----和当前函数大同小异,异在于会将晚轮插槽中的非高级总管定时器插入早轮中
                    return;
        }
    }
    (void) find_next_timeout(NULL, tiw); 更新下次触发时间  同样有利用scnt加快效率
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值