OC多线程学习(二) - GCD

2020-11-12  本文已影响0人  过气的程序员DZ

本文内容:

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

GCD概念


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

任务

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

函数

GCD中的函数总体分为:同步函数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下载地址

队列创建源码分析

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


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
            
    ......
}
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;
}

异步函数源码分析

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

dispatch_async(queue_c, ^{
    NSLog(@"12334");
});
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);
}
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);
}
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);
}
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);
}
上一篇下一篇

猜你喜欢

热点阅读