GCD源码分析5 —— dispatch_semaphore篇

前言

这篇文章主要分析dispatch_semaphore_t的底层实现

GCD信号量

dispatch_semaphore_t

dispatch_semaphore_s是性能稍次于自旋锁的的信号量对象,用来保证资源使用的安全性。

DISPATCH_DECL(dispatch_semaphore);

#define DISPATCH_DECL(name) typedef struct name##_s *name##_t;

struct dispatch_semaphore_s {
    DISPATCH_STRUCT_HEADER(dispatch_semaphore_s, dispatch_semaphore_vtable_s);
    long dsema_value;   // 当前信号值,当这个值小于0时无法访问加锁资源
    long dsema_orig;    // 初始化信号值,限制了同时访问资源的线程数量

    size_t dsema_sent_ksignals; //由于mach信号可能会被意外唤醒,通过原子操作来避免虚假信号

    semaphore_t dsema_port;
    semaphore_t dsema_waiter_port;
    size_t dsema_group_waiters;
    struct dispatch_sema_notify_s *dsema_notify_head;
    struct dispatch_sema_notify_s *dsema_notify_tail;
};
  • 1、semaphore_tmach_port_t信号
typedef mach_port_t semaphore_t;
  • 2、dispatch_sema_notify_s的定义如下:
struct dispatch_sema_notify_s {
    struct dispatch_sema_notify_s *dsn_next;
    dispatch_queue_t dsn_queue;
    void *dsn_ctxt;
    void (*dsn_func)(void *);
};

现在看起来还是一团散沙,硬着头皮往下看吧

dispatch_semaphore_create

创建一个具有初始值的计数信号量。如果有两个线程共同完成某一项工作时,可以使用一个初始值为0的semaphore。如果你需要管理一个有限的资源池时,应该使用一个初始值为资源池的大小的semaphore。如果不再使用这个semaphore时,应该使用dispatch_release销毁semaphore对象并且释放他的内存。(在ARC模式下不需要,系统会自动释放)

dispatch_semaphore_t dispatch_semaphore_create(long value){
    dispatch_semaphore_t dsema;

    if (value < 0) {
        return NULL;
    }
    // 申请内存
    dsema = calloc(1, sizeof(struct dispatch_semaphore_s));

    if (fastpath(dsema)) {
        // do_vtable里面主要包含了这个 dispatch_semaphore_s 的操作函数
        dsema->do_vtable = &_dispatch_semaphore_vtable;

        //可以理解为链表的结尾标记
        dsema->do_next = DISPATCH_OBJECT_LISTLESS;

        // 引用计数
        dsema->do_ref_cnt = 1;
        dsema->do_xref_cnt = 1;

        // 目标队列
        dsema->do_targetq = dispatch_get_global_queue(0, 0);

        // 信号值
        dsema->dsema_value = value;
        dsema->dsema_orig = value;
    }

    return dsema;
}
  1. 首先value必须大于等于0,流程才能继续进行;
  2. 申请一块dispatch_semaphore_s内存,并初始化为0;
  3. 将dsema的do_vtable指向_dispatch_semaphore_vtable_dispatch_semaphore_vtable的定义如下:
const struct dispatch_semaphore_vtable_s _dispatch_semaphore_vtable = {
    .do_type = DISPATCH_SEMAPHORE_TYPE,
    .do_kind = "semaphore",
    .do_dispose = _dispatch_semaphore_dispose,
    .do_debug = _dispatch_semaphore_debug,
};

_dispatch_semaphore_dispose,顾名思义,销毁一个信号量,定义如下:

void _dispatch_semaphore_dispose(dispatch_semaphore_t dsema){
    kern_return_t kr;

    if (dsema->dsema_value < dsema->dsema_orig) {
        DISPATCH_CLIENT_CRASH("Semaphore/group object deallocated while in use");
    }

    if (dsema->dsema_port) {
        kr = semaphore_destroy(mach_task_self(), dsema->dsema_port);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }
    if (dsema->dsema_waiter_port) {
        kr = semaphore_destroy(mach_task_self(), dsema->dsema_waiter_port);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
    }

    _dispatch_dispose(dsema);
}

dispatch_semaphore_wait

等待一个semaphore,或者是减少semaphore的计数。每次会执行会将计数器-1.如果减完之后计数器小于0的话,会阻塞在当前线程直到接收到信号。

long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout){

        // 如果信号量的值-1之后大于等于0,表示有资源可用
    if (dispatch_atomic_dec(&dsema->dsema_value) >= 0) {
        return 0;
    }
    return _dispatch_semaphore_wait_slow(dsema, timeout);
}

该函数先把信号量的信号值原子减1,如果大于等于0,则表示有资源可用,直接返回;否则调用_dispatch_semaphore_wait_slow,等到FIFO队列中信号量的到来,直到timeout为止。

static long _dispatch_semaphore_wait_slow(dispatch_semaphore_t dsema, dispatch_time_t timeout){
    mach_timespec_t _timeout;
    kern_return_t kr;
    uint64_t nsec;
    long orig;

again:
    _dispatch_semaphore_create_port(&dsema->dsema_port);

    switch (timeout) {
    default:
        do {
            // timeout() already calculates relative time left
            nsec = _dispatch_timeout(timeout);
            _timeout.tv_sec = (typeof(_timeout.tv_sec))(nsec / NSEC_PER_SEC);
            _timeout.tv_nsec = (typeof(_timeout.tv_nsec))(nsec % NSEC_PER_SEC);
            kr = slowpath(semaphore_timedwait(dsema->dsema_port, _timeout));
        } while (kr == KERN_ABORTED);

        if (kr != KERN_OPERATION_TIMED_OUT) {
            DISPATCH_SEMAPHORE_VERIFY_KR(kr);
            break;
        }
    // Fall through and try to undo what the fast path did to dsema->dsema_value
    case DISPATCH_TIME_NOW:
        while ((orig = dsema->dsema_value) < 0) {
            if (dispatch_atomic_cmpxchg(&dsema->dsema_value, orig, orig + 1)) {
                return KERN_OPERATION_TIMED_OUT;
            }
        }
    // Another thread called semaphore_signal().
        // Fall through and drain the wakeup.
    case DISPATCH_TIME_FOREVER:
        do {
            kr = semaphore_wait(dsema->dsema_port);
        } while (kr == KERN_ABORTED);
        DISPATCH_SEMAPHORE_VERIFY_KR(kr);
        break;
    }

    goto again;
}

进入_dispatch_semaphore_wait_slow之后,根据timeout的类型分三种情况:

  • 1、DISPATCH_TIME_NOW:检测后立即返回,不等待,若dsema_value小于0,那么将其增加1, 然后返回超时信号KERN_OPERATION_TIMED_OUT
  • 2、DISPATCH_TIME_FOREVER:无线等待,直到dsema_value大于0,若有了信号,会跳转到again,重新执行本函数的流程;
      /* 没有超时的代码 */
      kr = semaphore_wait(dsema->dsema_port);
    
  • 3、计时等待,直到有信号到来或者超时。
      /* 有超时的代码 */
      kr = slowpath(semaphore_timedwait(dsema->dsema_port, _timeout));
    

dispatch_semaphore_signal

发送信号,或者增加一个信号量的计数。如果增加之前的信号值dsema_value大于等于0,说明没有线程因为该信号而阻塞,所以直接返回。

DISPATCH_NOINLINE long dispatch_semaphore_signal(dispatch_semaphore_t dsema){
    if (dispatch_atomic_inc(&dsema->dsema_value) > 0) {
        return 0;
    }
    return _dispatch_semaphore_signal_slow(dsema);
}

如果增加之前的信号值小于0,则调用_dispatch_semaphore_signal_slow,将会唤起一个在spatch_semaphore_wait中等待的线程。(如果有多个线程在等待,iOS会根据线程的优先级来判断具体唤醒哪个线程)

DISPATCH_NOINLINE static long _dispatch_semaphore_signal_slow(dispatch_semaphore_t dsema){
    kern_return_t kr;

    _dispatch_semaphore_create_port(&dsema->dsema_port);

    // Before dsema_sent_ksignals is incremented we can rely on the reference
    // held by the waiter. However, once this value is incremented the waiter
    // may return between the atomic increment and the semaphore_signal(),
    // therefore an explicit reference must be held in order to safely access
    // dsema after the atomic increment.
    _dispatch_retain(dsema);

    dispatch_atomic_inc(&dsema->dsema_sent_ksignals);

    // 利用系统的信号量库实现发送信号量的功能
    kr = semaphore_signal(dsema->dsema_port);
    DISPATCH_SEMAPHORE_VERIFY_KR(kr);

    _dispatch_release(dsema);

    return 1;
}

总结

GCD信号量比较简单:记住这3个关键API:create、wait和signal即可

-------------本文结束 感谢您的阅读-------------

本文标题:GCD源码分析5 —— dispatch_semaphore篇

文章作者:lingyun

发布时间:2018年02月08日 - 13:02

最后更新:2018年02月12日 - 18:02

原始链接:https://tsuijunxi.github.io/2018/02/08/GCD源码分析5 —— dispatch-semaphore篇/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。