• skynet源码分析之消息队列


    skynet核心之一是消息队列,各个服务(skynet_context_xxx,ctx,是一个c结构)之间是通过消息进行通信。skynet包含全局队列和次级队列两级队列,skynet开启多个工作OS线程(可配置),每个线程不断的从全局队列里pop一个次级消息队列,然后分发次级消息队列里的消息,分发完后(可能只分发一条消息,也可能分发多条甚至全部消息)视情况是否push回全局队列,每个ctx有自己的次级队列,处理流程如图。下面分别讲解全局队列和次级队列的创建,入队,出队,释放操作。

    ( 图片来自于https://note.youdao.com/share/?id=9d2b8a03fdd9cd4947ca4128d30af420&type=note#/

    2. 全局队列

    skynet有一个全局队列global_mq,头尾指针分别指向一个次级队列,在skynet启动时初始化全局队列。

    struct global_queue { //全局队列结构
            struct message_queue *head; //指向一个ctx私有队列的头指针
            struct message_queue *tail; //指向一个ctx私有队列的尾指针
            struct spinlock lock; //自旋锁,保证同一时刻只有一个线程在处理
    };
    
    static struct global_queue *Q = NULL; //全局队列
    void 
    skynet_mq_init() { //全局队列初始化
        struct global_queue *q = skynet_malloc(sizeof(*q));
        memset(q,0,sizeof(*q));
        SPIN_INIT(q);
        Q=q;
    }

    在出队,入队操作时都加上自旋锁,保证同一时刻只有一个线程操作全局队列,保证线程安全。

    struct message_queue * 
    skynet_globalmq_pop() { //从全局队列pop一个私有队列
        struct global_queue *q = Q;
    
        SPIN_LOCK(q)
        ...
        SPIN_UNLOCK(q)
    
        return mq;
    }
    struct message_queue * 
    skynet_globalmq_pop() { //从全局队列pop一个私有队列
        struct global_queue *q = Q;
    
        SPIN_LOCK(q)
            ...  
        SPIN_UNLOCK(q)
    
        return mq;
    }

    3. 私有队列

    每个服务ctx有一个私有消息队列message_queue(mq),为了防止cpu空转,当mq没消息时,是不会push到全局队列里的。

    struct message_queue { //每个ctx私有队列结构
        struct spinlock lock; //自旋锁,保证最多只有一个线程在处理
        uint32_t handle; //对应的ctx,注意是个整数,而不是指针
        int cap; //队列容量(数组长度)
        int head; //队列头
        int tail; //队列尾
        int release; //标记是否可释放(当delete ctx时会设置此标记)
        int in_global; //标记是否在全局队列中,1表示在j
        int overload; //标记是否过载
        int overload_threshold; //过载阈值,初始是MQ_OVERLOAD
        struct skynet_message *queue; //消息数据,实际上是一个数组,通过head,tail实现类似队列的功能
        struct message_queue *next; //指向下一个消息队列
    };

    在创建ctx期间被创建,ctx里包含一个mq指针,mq里包含ctx对应的handle。初看是一个队列,但本质上是数组,数组容量为cap,用数组实现队列的入队出队操作,head、tail分别索引队列头部,尾部。

    struct message_queue * 
    skynet_mq_create(uint32_t handle) { //创建一个私有队列,当创建一个ctx会调用,handle对应ctx
        struct message_queue *q = skynet_malloc(sizeof(*q)); //分配内存
        q->handle = handle;
        q->cap = DEFAULT_QUEUE_SIZE; //初始queue容量
        q->head = 0;
        q->tail = 0;
        SPIN_INIT(q) //初始化自旋锁
        // When the queue is create (always between service create and service init) ,
        // set in_global flag to avoid push it to global queue .
        // If the service init success, skynet_context_new will call skynet_mq_push to push it to global queue.
        // 创建队列时可以发送和接收消息,但还不能被工作线程调度,所以设置成MQ_IN_GLOBAL,保证不会push到全局队列,
        // 当ctx初始化完成再直接调用skynet_globalmq_push到全局队列
        q->in_global = MQ_IN_GLOBAL;
        q->release = 0;
        q->overload = 0;
        q->overload_threshold = MQ_OVERLOAD;
        q->queue = skynet_malloc(sizeof(struct skynet_message) * q->cap); //分配cap个skynet_message大小容量
        q->next = NULL;
    
        return q;
    }

    入队操作,当数组满(head==tail)时,扩充数组容量,in_global标记此私有队列是否在全局队列里,若已经存在则不需要push到全局队列,这样当某个工作线程调度到(pop)这个mq后,全局队列里不会再存在同一mq,从而不会被其他线程调度掉,保证了线程安全。若不在全局队列里,则push到全局队列,供工作线程调度。

    skynet_mq_push(struct message_queue *q, struct skynet_message *message) { //向消息队列里push消息
        assert(message);
        SPIN_LOCK(q)
    
        q->queue[q->tail] = *message; //存到尾部,然后尾部+1,如果超过容量,则重置为0
        if (++ q->tail >= q->cap) {
            q->tail = 0;
        }
    
        if (q->head == q->tail) { //如尾部==头部,说明队列已满,需扩充容量
            expand_queue(q);
        }
    
        if (q->in_global == 0) { //如不在全局队列里,则push到全局队列
            q->in_global = MQ_IN_GLOBAL;
            skynet_globalmq_push(q);
        }
        
        SPIN_UNLOCK(q)
    }

    出队操作,当head超出数组容量时,重置head为0。对mq的操作都会加上自旋锁,保证线程安全。虽然只能被一个工作线程调度到,然后从中pop一条消息进行分发,但若不加锁这期间其他线程可以向此mq push消息。

    int
    skynet_mq_pop(struct message_queue *q, struct skynet_message *message) { //从私有队列里pop一个消息
        SPIN_LOCK(q)
        ...   
        if (head >= cap) { //大于容量,重置为0
            q->head = head = 0;
        }
        ...
        SPIN_UNLOCK(q)
        return ret;
    }

    释放操作,工作线程分发消息是通过mq里的handle找到对应的ctx,然后调用ctx的callback函数。此时,若找不到ctx(ctx被delete),则需要释放mq,释放内存前需要处理mq里消息,防止消息发送方一直等待。由此可见,mq的生命周期是跟随ctx的。注意:不一定能马上释放掉mq,只有release标记为真时才能释放,原因参考云风大神的博客 https://blog.codingnow.com/2012/08/skynet_bug.html

    static void
    _drop_queue(struct message_queue *q, message_drop drop_func, void *ud) { //准备释放队列 struct skynet_message msg; while(!skynet_mq_pop(q, &msg)) { //先向队列里各个消息的源地址发送特定消息,再释放内存 drop_func(&msg, ud); } _release(q); } void skynet_mq_release(struct message_queue *q, message_drop drop_func, void *ud) { //尝试释放私有队列 SPIN_LOCK(q) //只有队列已经被标记可释放(release==1)时,说明ctx真正delete了,才能释放掉队列, //否则继续push到全局队列,等待下一次调度 if (q->release) { SPIN_UNLOCK(q) _drop_queue(q, drop_func, ud); } else { skynet_globalmq_push(q); SPIN_UNLOCK(q) } }
  • 相关阅读:
    SpringBoot启动方式
    自制反汇编逆向分析工具
    libdispatch.dylib中dispatch_group的实现
    深入ObjC GCD中的dispatch group工作原理。
    objc反汇编分析,手工逆向libsystem_blocks.dylib
    UML分析AsyncDisplayKit框架-ASMuplexImageNode异步下载时序图。
    objc反汇编分析,block函数块为何物?
    apple平台下的objc的GCD,多线程编程就是优雅自然。
    AsyncDisplayKit编译和使用注意事项
    反汇编objc分析__block
  • 原文地址:https://www.cnblogs.com/RainRill/p/8252480.html
Copyright © 2020-2023  润新知