美文网首页
OC多线程学习(二) - GCD

OC多线程学习(二) - GCD

作者: 过气的程序员DZ | 来源:发表于2020-11-12 23:13 被阅读0次

    本文内容:

    1. GCD相关概念
    2. 有关GCD的几道面试题
    3. 源码分析:队列和异步函数

    GCD概念


    GCD是Grand Central Dispatch的缩写。是苹果为提供多核并行运算而提出的解决方案。主要功能作用:将任务添加到队列,并且指定执行任务的函数。而且开发人员不需要编写管理线程生命周期的代码。

    任务

    GCD中的任务用block封装,并有以下特点:

    • 任务block没有参数也没有返回值
    • 不需要手动调用block,GCD内部帮我们调用

    函数

    GCD中的函数总体分为:同步函数dispatch_sync和异步函数dispatch_async

    • 同步函数dispatch_sync
      • 等待当前语句执行完毕
      • 不会开启线程
      • 在当前线程执行任务
    • 异步函数dispatch_async
      • 不用等待当前语句执行完毕
      • 会开启线程新线程执行任务

    队列

    队列是一种数据结构。具有先进先出的特性。GCD中大致分为两种队列类型,串行队列并发队列

    根据调用不同的函数(同步or异步),会有以下四种情况:

    --- 同步函数 异步函数
    串行队列 1.不会开启线程 2.任务按顺序执行 3.会产生堵塞 1.开启新线程 2.任务按顺序执行
    并发队列 1.不会开启线程 2.任务按顺序执行 1.开启新线程 2.任务异步执行,没有顺序,与CPU的调度有关

    队列和线程的关系

    面试的时候经常会被问到队列和线程之间的关系?
    其实他们是没有太大的关系的,队列是一种数据结构,作为任务的容器。线程是进程的基本执行单元,是任务的执行者。CPU调度线程去执行容器中的任务。所以说队列和线程没有直接关系,只是在不同业务层级中担当不同的角色罢了。

    GCD一些相关面试题


    面试题1:
    dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
    NSLog(@"1");
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_async(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
    

    答案:1、5、2、4、3
    分析:队列是并行队列,两次调用异步函数(dispatch_async),都会开启新的线程执行任务,并且不会堵塞当前线程。

    1. 首先打印“1”,遇到异步函数不处理,然后打印“5”。
    2. 第一层异步函数内执行逻辑与外部类似,打印“2”和“4”。
    3. 最后执行第二次异步函数,打印“3”
    面试题2
    dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_CONCURRENT);
        
    dispatch_async(queue, ^{
        // sleep(2);
        NSLog(@"1");
    });
    dispatch_async(queue, ^{
        NSLog(@"2");
    });
    // 堵塞
    dispatch_sync(queue, ^{
        NSLog(@"3");
    });
    // **********************
    NSLog(@"0");
    
    dispatch_async(queue, ^{
        NSLog(@"7");
    });
    dispatch_async(queue, ^{
        NSLog(@"8");
    });
    dispatch_async(queue, ^{
        NSLog(@"9");
    });
    // A: 1230789
    // B: 1237890
    // C: 3120798
    // D: 2137890
    

    答案:AC
    分析:并行队列添加相关任务,其中“3”是同步函数,会堵塞(堵塞的代码行在同步函数代码行结束的位置,也就是当前代码NSLog(@"3");下一行的“});”)当前线程。“0”在主线程中执行,其他的都是异步函数,所以“0”后面的异步函数肯定都会在“0”之后执行。因此本题答案是“3”在“0”之前,并且“7”、“8”、“9”在“0”之后。因此答案是AC。注意:“1”和“2”的位置不确定,这个取决于任务的时间复杂度,可以打开“1”中的sleep,打印查看一下结果,“1”会在“9”之后打印。

    面试题3:
    // 串行队列
    dispatch_queue_t queue = dispatch_queue_create("HelloGCD", DISPATCH_QUEUE_SERIAL);
    NSLog(@"1");
    // 异步函数
    dispatch_async(queue, ^{
        NSLog(@"2");
        dispatch_sync(queue, ^{
            NSLog(@"3");
        });
        NSLog(@"4");
    });
    NSLog(@"5");
    

    答案:1、5、2、崩溃(EXC_BAD_INSTRUCTION)
    分析:

    1. 在主线程队列中(串行队列),依次加入“1”、异步函数(dispatch_async)代码块、“5”
    2. 异步函数开启子线程,不阻塞主线程,所以先打印“1”和“5”。
    3. 子线程中,由于是串行队列,所以会把“2”、同步函数dispatch_sync、“4”这三个“任务”依次加入到queue中。
    4. 子线程开始串行执行任务,打印“2”
    5. 队列下一个任务是同步函数,会阻塞当前队列,然后把“3”加入到队列中,此时会产生死锁,此时队列情况:==dispatch_sync的block - “4” - “3”==
      • 同步函数需要“3”执行完,自己才能执行结束。
      • 由于“3”是在“4”后面加入到队列,所以“3”要等待“4”执行完成。
      • “4”在同步函数后面加入到队列,所以得等待同步函数执行结束。
      • 等待情况:dispatch_sync - “3” - “4” - dispatch_sync,是互相等待的状态,因此出现了死锁。
    面试题4
    __block int a = 0;
    while (a < 5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            a++;
        });
    }
        
    NSLog(@"out a = %d", a);
    

    问题1:最后的打印
    A:0 B:<5 C:=5 D:>5
    答案:CD
    分析:
    a初始化=0,进入while循环,循环条件是a<5,所以当a小于5的时候,都会在while循环中,所以可以排除A和B。循环中使用的是异步函数,异步函数会开辟线程,所以当其中一个线程的操作a++后满足跳出循环的条件了,就会退出循环,但是此时可能还会有其他线程还没有执行完,就会有a>=5的情况。因此答案是CD。

    问题2:如何获取到循环中最后的a值
    答案:在while循环外,使用相同的队列中,再次调用异步函数。

    ...
    
    NSLog(@"out a = %d", a);
    // add code 
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        sleep(1);
        NSLog(@"out a = %d", a);
    });
    // end add
    

    分析:
    在相同队列中,以相同的方式(异步函数)再追加一个任务,任务内容是打印a,为了队列中其他任务执行完毕,此处增加一个sleep,因为任务都比较简单(NSLog),就算不加也不会有太大的问题。
    这个问题在正常开发中不会使用到,而且会浪费很大的性能(会有很多无用的线程执行无用的任务)。目的只是考餐对GCD队列的了解程度。

    问题3:如何进行性能优化
    答案:
    用信号量加锁的方式

    dispatch_semaphore_t s = dispatch_semaphore_create(1);
    __block int a = 0;
    while (a < 5) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"in a = %d, %@", a, [NSThread currentThread]);
            a++;
            dispatch_semaphore_signal(s);
        });
        dispatch_semaphore_wait(s, DISPATCH_TIME_FOREVER);
    }
        
    NSLog(@"out a = %d", a);
    

    分析:
    此处信号量wait方法,如果放在异步函数(dispatch_async)调用之前,那么异步函数的就没有使用的意义了(编程顺序执行,可以把异步函数的代码删掉了)。放在异步函数之后,异步函数还是有意义的,不懂的可以自己打印看看打印结果。信号量加锁的方式很容易理解,但是两个函数放的位置,还有根据具体的业务需求来自行决定。

    底层分析

    源码libdispatch下载地址

    队列创建源码分析

    队列也是对象,通过一个示例证明一下:


    • 代码中创建两个队列对象,一个是串行队列,另一个是并发队列
    • 通过runtime的api方法object_getClass,查看他们的归属类:
      • 串行队列:OS_dispatch_queue_serial
      • 并发队列:OS_dispatch_queue_concurrent
    • 接下来通过查看源码找到类名创建队列的地方,以及isa的指向
    dispatch_queue_t
    dispatch_queue_create(const char *label, dispatch_queue_attr_t attr)
    {
        return _dispatch_lane_create_with_target(label, attr,
                DISPATCH_TARGET_QUEUE_DEFAULT, true);
    }
    
    static dispatch_queue_t
    _dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
            dispatch_queue_t tq, bool legacy)
    {
    
        dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
        
        ......
        
        const void *vtable;
        if (dqai.dqai_concurrent) {
            vtable = DISPATCH_VTABLE(queue_concurrent);
        } else {
            vtable = DISPATCH_VTABLE(queue_serial);
        }
        
        ......
        
        dispatch_lane_t dq = _dispatch_object_alloc(vtable,
                sizeof(struct dispatch_lane_s)); // alloc
        _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0)); // init
                
        ......
    }
    
    • dispatch_queue_create函数有两个参数:
      • 第一个label:字符标示
      • 第二个attr:表示串行队列还是并发队列
    • 紧接着调用_dispatch_lane_create_with_target函数,前两个参数就是dispatch_queue_create的两个参数,第二个参数dqa就是attr
    • dqa封装成dispatch_queue_attr_info_t类型,变量是dqai,这里对传入的参数进行了判断,赋值给dqai.dqai_concurrent,然后DISPATCH_VTABLE宏获取不同队列的类名存入到vtable变量中
    • 后续调用_dispatch_object_alloc进行内存分配
    void *
    _dispatch_object_alloc(const void *vtable, size_t size)
    {
    #if OS_OBJECT_HAVE_OBJC1
    //不关心的代码
        .....
    #else
        return _os_object_alloc_realized(vtable, size);
    #endif
    }
    
    inline _os_object_t
    _os_object_alloc_realized(const void *cls, size_t size)
    {
        _os_object_t obj;
        dispatch_assert(size >= sizeof(struct _os_object_s));
        while (unlikely(!(obj = calloc(1u, size)))) {
            _dispatch_temporary_resource_shortage();
        }
        obj->os_obj_isa = cls;//isa赋值
        return obj;
    }
    
    • 调用_dispatch_object_alloc函数,间接调用_os_object_alloc_realized函数
    • _os_object_alloc_realized中看到了isa赋值代码:obj->os_obj_isa = cls;
    • 到此就我们就了解队列对象的整个初始化过程。

    异步函数源码分析

    主要的研究目标是任务block是如何被调用的。

    dispatch_async(queue_c, ^{
        NSLog(@"12334");
    });
    
    • dispatch_async有两个参数,第一个参数是队列,第二个是任务(block)
    void
    dispatch_async(dispatch_queue_t dq, dispatch_block_t work)
    {
        dispatch_continuation_t dc = _dispatch_continuation_alloc();
        uintptr_t dc_flags = DC_FLAG_CONSUME;
        dispatch_qos_t qos;
    
        qos = _dispatch_continuation_init(dc, dq, work, 0, dc_flags);
        _dispatch_continuation_async(dq, dc, qos, dc->dc_flags);
    }
    
    • work参数被传入到_dispatch_continuation_init函数中的第三个参数,其余的地方没有用到
    static inline dispatch_qos_t
    _dispatch_continuation_init(dispatch_continuation_t dc,
            dispatch_queue_class_t dqu, dispatch_block_t work,
            dispatch_block_flags_t flags, uintptr_t dc_flags)
    {
        //封装work成ctxt
        void *ctxt = _dispatch_Block_copy(work);
    
        dc_flags |= DC_FLAG_BLOCK | DC_FLAG_ALLOCATED;
        if (unlikely(_dispatch_block_has_private_data(work))) {
            dc->dc_flags = dc_flags;
            dc->dc_ctxt = ctxt;
            // will initialize all fields but requires dc_flags & dc_ctxt to be set
            return _dispatch_continuation_init_slow(dc, dqu, flags);
        }
    
        //封装work成func
        dispatch_function_t func = _dispatch_Block_invoke(work);
        if (dc_flags & DC_FLAG_CONSUME) {
            func = _dispatch_call_block_and_release;
        }
        //ctxt和func作为参数传入
        return _dispatch_continuation_init_f(dc, dqu, ctxt, func, flags, dc_flags);
    }
    
    • work被封装成ctxtfunc,然后传入到_dispatch_continuation_init_f函数中
    static inline dispatch_qos_t
    _dispatch_continuation_init_f(dispatch_continuation_t dc,
            dispatch_queue_class_t dqu, void *ctxt, dispatch_function_t f,
            dispatch_block_flags_t flags, uintptr_t dc_flags)
    {
        pthread_priority_t pp = 0;
        dc->dc_flags = dc_flags | DC_FLAG_ALLOCATED;
        dc->dc_func = f;//保存f
        dc->dc_ctxt = ctxt;//保存ctxt
        // in this context DISPATCH_BLOCK_HAS_PRIORITY means that the priority
        // should not be propagated, only taken from the handler if it has one
        if (!(flags & DISPATCH_BLOCK_HAS_PRIORITY)) {
            pp = _dispatch_priority_propagate();
        }
        _dispatch_continuation_voucher_set(dc, flags);
        return _dispatch_continuation_priority_set(dc, dqu, pp, flags);
    }
    
    • 到此我们看到了任务block的保存(dc->dc_ctxt = ctxt)和调用函数的保存(dc->dc_func = f),那么在什么时候调用呢?我们就需要查看调用堆栈了
    • 在任务block内下断点,然后bt命令查看调用栈。
    void
    _dispatch_client_callout(void *ctxt, dispatch_function_t f)
    {
        @try {
            return f(ctxt);//之前保存的相关调用方法和任务
        }
        @catch (...) {
            objc_terminate();
        }
    }
    
    void
    _dispatch_call_block_and_release(void *block)
    {
        void (^b)(void) = block;
        b();
        Block_release(b);
    }
    
    • 可以看到最后调用的是_dispatch_call_block_and_release函数。这个函数就是上面源码中保存的fdc->dc_func = f;
    • 此时就可以清楚为什么GCD相关的任务block不用我们手动调用了。

    相关文章

      网友评论

          本文标题:OC多线程学习(二) - GCD

          本文链接:https://www.haomeiwen.com/subject/gapmbktx.html