dispatch_once浅谈

来源:互联网 发布:小米6相机在那清除数据 编辑:程序博客网 时间:2024/05/21 06:38

让我们先看看dispatch_once的实现(Grand Central Dispatch是开源的,大家可以到git://git.macosforge.org/libdispatch.git克隆源码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
struct_dispatch_once_waiter_s {
    volatilestruct _dispatch_once_waiter_s *volatiledow_next;
    _dispatch_thread_semaphore_tdow_sema;
};
 
#define DISPATCH_ONCE_DONE ((struct _dispatch_once_waiter_s *)~0l)
 
#ifdef __BLOCKS__
void
dispatch_once(dispatch_once_t*val, dispatch_block_tblock)
{
    structBlock_basic *bb = (void*)block;
 
    dispatch_once_f(val, block, (void*)bb->Block_invoke);
}
#endif
 
DISPATCH_NOINLINE
void
dispatch_once_f(dispatch_once_t*val, void*ctxt, dispatch_function_tfunc)
{
    struct_dispatch_once_waiter_s * volatile*vval = (struct_dispatch_once_waiter_s**)val;
    struct_dispatch_once_waiter_s dow = { NULL,0};
    struct_dispatch_once_waiter_s *tail, *tmp;
    _dispatch_thread_semaphore_tsema;
 
    if(dispatch_atomic_cmpxchg(vval, NULL, &dow)) {
        dispatch_atomic_acquire_barrier();//这是一个空的宏函数,什么也不做
        _dispatch_client_callout(ctxt, func);
        dispatch_atomic_maximally_synchronizing_barrier();
        //dispatch_atomic_release_barrier(); // assumed contained in above
        tmp = dispatch_atomic_xchg(vval, DISPATCH_ONCE_DONE);
        tail = &dow;
        while(tail != tmp) {
            while(!tmp->dow_next) {
                _dispatch_hardware_pause();
            }
            sema = tmp->dow_sema;
            tmp = (struct_dispatch_once_waiter_s*)tmp->dow_next;
            _dispatch_thread_semaphore_signal(sema);
        }
    }else{
        dow.dow_sema = _dispatch_get_thread_semaphore();
        for(;;) {
            tmp = *vval;
            if(tmp == DISPATCH_ONCE_DONE) {
                break;
            }
            dispatch_atomic_store_barrier();
            if(dispatch_atomic_cmpxchg(vval, tmp, &dow)) {
                dow.dow_next = tmp;
                _dispatch_thread_semaphore_wait(dow.dow_sema);
            }
        }
        _dispatch_put_thread_semaphore(dow.dow_sema);
    }
}

一堆宏函数加一堆让人头大的线程同步代码。一步一步看:

dispatch_once内部其实是调用了dispatch_once_f,f指的是调用c函数(没有f指的是调用block),实际上执行block最终也是调用c函数(详见我的《Block非官方编程指南》)。当dispatch_once_f被调用时,val是外部传入的predicatectxt传入的是Block的指针,func指的是Block内部的执行体函数,执行它就是执行block。

接下来是声明了一堆变量,vval是volatile标记过的val,volatile修饰符的作用上一篇已经介绍过,告诉编译器此指针指向的值随时可能被其他线程改变,从而使得编译器不对此指针进行代码编译优化。

dow意为dispatch_once wait

dispatch_atomic_cmpxchg是上一篇我们讲过的“原子比较交换函数”__sync_bool_compare_and_swap的宏替换,接下来进入分支:

1.执行block的分支

dispatch_once第一次执行时,predicate也即val为0,那么此“原子比较交换函数”将返回true并将vval指向值赋值为&dow,即为“等待中”,_dispatch_client_callout其内部做了一些判定,但实际上是调用了func而已。到此,block中的用户代码执行完毕。

接下来就是上篇提及的cpuid指令等待,使得其他线程的【读取到未初始化值的】预执行能被判定为猜测未命中,从而使得这些线程能够进入dispatch_once_f里的另一个分支从而进行等待。

cpuid指令完毕后,调用dispatch_atomic_xchg进行赋值,置其为DISPATCH_ONCE_DONE,即“完成”,这里dispatch_atomic_xchg是内建“原子交换函数”__sync_swap的优化版宏替换,其将第二个参数的值赋给第一个参数(解引用指针),然后返回第一个参数被赋值前的解引用值,其原型为:

1
type __sync_swap(type *ptr, type value, ...)

接下来是对信号量链的处理:

  1. 在block执行过程中,没有其他线程进入本函数来等待,则vval指向值保持为&dow,即tmp被赋值为&dow,即下方while循环不会被执行,此分支结束。
  2. 在block执行过程中,有其他线程进入本函数来等待,那么会构造一个信号量链表(vval指向值变为信号量链的头部,链表的尾部为&dow),此时就会进入while循环,在此while循环中,遍历链表,逐个signal每个信号量,然后结束循环。

while (!tmp->dow_next)此循环是等待在&dow上,因为线程等待分支#2会中途将val赋值为&dow,然后为->dow_next赋值,这期间->dow_next值为NULL,需要等待,详见下面线程等待分支#2的描述

_dispatch_hardware_pause此句是为了提示cpu减少额外处理,提升性能,节省电力。

2.线程等待分支

执行block分支#1未完成,且有线程再进入本函数时,将进入线程等待分支:

先调用_dispatch_get_thread_semaphore创建一个信号量,此信号量被赋值给dow.dow_sema

然后进入一个无限for循环,假如发现vval的指向值已经为DISPATCH_ONCE_DONE,即“完成”,则直接break,然后调用_dispatch_put_thread_semaphore函数销毁信号量并退出函数。

_dispatch_get_thread_semaphore内部使用的是“有即取用,无即创建”策略来获取信号量。

_dispatch_put_thread_semaphore内部使用的是“销毁旧的,存储新的”策略来缓存信号量。

假如vval的解引用值并非DISPATCH_ONCE_DONE,则进行一个“原子比较并交换”操作(此操作可以避免两个等待线程同时操作链表带来的问题),假如此时vval指向值已不再是tmp(这种情况发生在多个线程同时进入线程等待分支#2,并交错修改链表)则for循环重新开始,再尝试重新获取一次vval来进行同样的操作;若指向值还是tmp,则将vval的指向值赋值为&dow,此时val->dow_next值为NULL,可能会使得block执行分支#1进行while等待(如前述),紧接着执行dow.dow_next = tmp这句来增加链表节点(同时也使得block执行分支#1的while等待结束),然后等待在信号量上,当block执行分支#1完成并遍历链表来signal时,唤醒、释放信号量,然后一切就完成了。

小结

综上所述,dispatch_once的主要处理的情况如下:

  1. 线程A执行Block时,任何其它线程都需要等待。
  2. 线程A执行完Block应该立即标记任务完成状态,然后遍历信号量链来唤醒所有等待线程。
  3. 线程A遍历信号量链来signal时,任何其他新进入函数的线程都应该直接返回而无需等待。
  4. 线程A遍历信号量链来signal时,若有其它等待线程B仍在更新或试图更新信号量链,应该保证此线程B能正确完成其任务:a.直接返回 b.等待在信号量上并很快又被唤醒。
  5. 线程B构造信号量时,应该考虑线程A随时可能改变状态(“等待”、“完成”、“遍历信号量链”)。
  6. 线程B构造信号量时,应该考虑到另一个线程C也可能正在更新或试图更新信号量链,应该保证B、C都能正常完成其任务:a.增加链节并等待在信号量上 b.发现线程A已经标记“完成”然后直接销毁信号量并退出函数。

总结

无锁的线程同步编程非常精巧,为了提升效率,每一处线程竞争都必须被考虑到并妥善处理。但这种编程方式又极其令人神往,原子操作的魅力便在于此,它就像是一个精密的钟表,每一处接合都如此巧妙

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 凤尾蕨叶子蔫了怎么办 绿地珊瑚蕨干了怎么办 翠云草叶子蔫了怎么办 珊瑚蕨叶子烂了怎么办 黑骨茶叶子黑斑怎么办 外场主持没有人互动怎么办 企业年报填错了怎么办 手机忘了放哪了怎么办 燃气卡车没气了怎么办 老房子拆了重建怎么办 加了差的汽油怎么办 事业单位未满5年怎么办 小学孩子不会制订学习计划怎么办 忘记提交原创怎么办百度熊掌号 计算机一级报名表填错了怎么办 离婚了不给孩子生活费怎么办 点击爱奇艺系统提示停止运行怎么办 电视机图像颜色变了怎么办 10万签约被拒怎么办 钢琴弹奏中的折指怎么办! 吉他琴头旋钮将琴头扭坏了怎么办? 1岁幼儿不爱吃饭怎么办 3岁幼儿不爱吃饭怎么办 4岁幼儿不爱吃饭怎么办 买票买了点映 怎么办 一年级孩子注意力不集中怎么办 孩子上一年级注意力不集中怎么办 普宁二中初一新生住宿怎么办 小学入学普查错过了怎么办 初一的孩子不爱学习怎么办 初一孩子出现厌学情况怎么办 客人要求我们代办事项时怎么办 客人要求我们代办事项应该怎么办 想家了特别想哭怎么办 把友谊看得太重怎么办 高一新生数学差怎么办 如果好朋友觉得你成熟阴暗怎么办 被同学抓住把柄敲诈怎么办 初一学生字写的不好怎么办 初二学生字写不好怎么办 宝宝流鼻涕怎么办最简单方法