美文网首页iOS-机制
iOS-多线程(二)-GCD基础

iOS-多线程(二)-GCD基础

作者: xxxxxxxx_123 | 来源:发表于2020-03-03 19:37 被阅读0次

    简介

    什么是GCD?

    GCD的全称是Grand Central Dispatch,它是Apple 开发的一个多核编程的解决方法,由纯C语言实现,提供了非常强大的函数,用来对多线程进行相关的操作。

    GCD的优势

    • GCD会自动利用更多的CPU内核(比如双核、四核)
    • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程) 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

    任务和队列

    GCD当中,加入了两个比较重要的概念:任务(task)和队列(queue)。任务就是我们要执行的操作,而队列则表明了多个操作的执行方法。简而言之,GCD的核心就是将任务添加到队列,并且指定函数执行任务。

    任务

    任务使用block封装,该block没有参数也没有返回值。

    typedef void (^dispatch_block_t)(void);
    
    dispatch_block_t block = ^{
        
    };
    

    执行任务有两种方式:同步异步。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

    • 同步执行(sync):
      • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
      • 只能在当前线程中执行任务,不具备开启新线程的能力。
    dispatch_sync(, { ()-> Void in
    
    })
    
    • 异步执行(async):
      • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
      • 可以在新的线程中执行任务,具备开启新线程的能力。
    dispatch_async(, { ()-> Void in
    
    })
    

    所以同步任务会阻塞当前线程并等待block中的任务执行完毕,才会执行下一个任务;而异步任务则不用等待当前语句执行完毕,就可以执行下一条语句,并不会出现前后任务互相阻塞等待的情况。

    需要注意的是异步(async)虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

    队列

    队列:用于存放任务。GCD中有两种队列:串行队列和并行队列。

    • 串行队列(Serial Dispatch Queue):
      每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
    • 并发队列(Concurrent Dispatch Queue):
      可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
    image

    由于队列和任务时搭配使用,所以产生了下面四种方法:

      1. 同步函数串行队列:
      • 不会开启线程,在当前线程执行任务
      • 任务串行执行,任务一个接着一个
      • 会产生堵塞
      1. 同步函数并发队列
      • 不会开启线程,在当前线程执行任务
      • 任务一个接着一个
      1. 异步函数串行队列
      • 开启线程一条新线程
      • 任务一个接着一个
      1. 异步函数并发队列
      • 开启线程,在当前线程执行任务
      • 任务异步执行,没有顺序,和CPU调度有关

    GCD的概念

    GCD的使用步骤很简单,首先创建一个队列(串行队列或并发队列),然后将任务追加到任务的等待队列中执行任务(同步执行或异步执行)。

    队列的创建

    #define DISPATCH_TARGET_QUEUE_DEFAULT NULL
    
    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);
    }
    

    其参数如下:

    • const char *label: 队列的唯一标识符,可以传空值。
    • dispatch_queue_attr_t attr: 标识队列的类型,区分是串行队列还是并发队列。
      • DISPATCH_QUEUE_SERIAL: 串行队列
      • DISPATCH_QUEUE_CONCURRENT: 并发队列

    串行队列的创建方法

    dispatch_queue_t queue = dispatch_queue_create("serial_queue", DISPATCH_QUEUE_SERIAL);
    

    常用的主队列就是串行队列,dispatch_get_main_queue()

    • 专门用在主线程调度任务的队列,也称UI队列
    • 不会再开启线程
    • 如果有任务执行,再添加其他任务,会被堵塞

    其实主队列其实并不特殊。只是默认情况下,如果没有开别的线程,程序都是放在主队列中的,而主队列又都会放到主线程中去执行,所以才造成了主队列特殊的现象。

    并发队列的创建方法

    dispatch_queue_t queue = dispatch_queue_create("concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
    

    常用的全局队列就是并发队列dispatch_get_global_queue(long identifier, unsigned long flags),它可以直接执行异步任务。该方法第一个参数是优先级,全局队列的优先级为DISPATCH_QUEUE_PRIORITY_DEFAULT,这个值是一个为0的宏,所以也可以传0。unsigned long flags: 苹果官方文档的解释是Flags that are reserved for future use。标记这个参数是为了未来使用保留的,现在传0即可。

    此处引入线程的优先级概念,优先级越高越先执行。

    • DISPATCH_QUEUE_PRIORITY_HIGH: 2
    • DISPATCH_QUEUE_PRIORITY_DEFAULT: 0
    • DISPATCH_QUEUE_PRIORITY_LOW: (-2)
    • DISPATCH_QUEUE_PRIORITY_BACKGROUND: INT16_MIN

    我们接着看看队列到底是如何创建的?

    首先我们生成以下代码,并在控制台输出:

    image

    然后我们跟踪代码,去看看创建的过程。当我们创建一个并发或者串行队列的时候,最终会进入以下代码:

    DISPATCH_NOINLINE
    static dispatch_queue_t
    _dispatch_lane_create_with_target(const char *label, dispatch_queue_attr_t dqa,
            dispatch_queue_t tq, bool legacy)
    {   
        // 通过dqai.dqai_concurrent来判断
        // dqai如果是空的结构体就是串行队列,如果有值就是并发队列
        // 不管串行或者并发,tq都是DISPATCH_TARGET_QUEUE_DEFAULT == NULL
        // 串行队列的 dqa == NULL,并发有值
        dispatch_queue_attr_info_t dqai = _dispatch_queue_attr_to_info(dqa);
        
        // 串行队列的 dqai = {},并发有值
    
        ......
        
        _dispatch_queue_attr_overcommit_t overcommit = dqai.dqai_overcommit;
    
        ......
        if (overcommit == _dispatch_queue_attr_overcommit_unspecified) {   
            // 给overcommit赋值
            // 并发队列的overcommit为_dispatch_queue_attr_overcommit_disabled
            // 串行队列的overcommit为_dispatch_queue_attr_overcommit_enabled
            overcommit = dqai.dqai_concurrent ?
                        _dispatch_queue_attr_overcommit_disabled :
                        _dispatch_queue_attr_overcommit_enabled;
        }
        if (!tq) {
            // 设置tq
            // DISPATCH_QOS_UNSPECIFIED = 0 DISPATCH_QOS_DEFAULT = 4
            // 创建的时候qos = 0
            // _dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
            // 第一个参数固定是4  第二个参数串行队列为true、并发队列为false
            tq = _dispatch_get_root_queue(
                    qos == DISPATCH_QOS_UNSPECIFIED ? DISPATCH_QOS_DEFAULT : qos, // 4
                    overcommit == _dispatch_queue_attr_overcommit_enabled)->_as_dq; // 0 1
            if (unlikely(!tq)) {
                DISPATCH_CLIENT_CRASH(qos, "Invalid queue attribute");
            }
        }
    
       
        // 开辟内存 - 生成响应的对象 queue
        dispatch_lane_t dq = _dispatch_object_alloc(vtable,
                sizeof(struct dispatch_lane_s));
        
        // 构造方法
        _dispatch_queue_init(dq, dqf, dqai.dqai_concurrent ?
                DISPATCH_QUEUE_WIDTH_MAX : 1, DISPATCH_QUEUE_ROLE_INNER |
                (dqai.dqai_inactive ? DISPATCH_QUEUE_INACTIVE : 0));
    
        // 标签
        dq->dq_label = label;
        // 优先级
        dq->dq_priority = _dispatch_priority_make((dispatch_qos_t)dqai.dqai_qos,
                dqai.dqai_relpri);
        if (overcommit == _dispatch_queue_attr_overcommit_enabled) {
            dq->dq_priority |= DISPATCH_PRIORITY_FLAG_OVERCOMMIT;
        }
        if (!dqai.dqai_inactive) {
            _dispatch_queue_priority_inherit_from_target(dq, tq);
            _dispatch_lane_inherit_wlh_from_target(dq, tq);
        }
        _dispatch_retain(tq);
        // 将tq的值赋给targetq
        dq->do_targetq = tq;
        _dispatch_object_debug(dq, "%s", __func__);
        return _dispatch_trace_queue_create(dq)._dq;
    }
    
    // 通过dqa获取到dqai
    dispatch_queue_attr_info_t
    _dispatch_queue_attr_to_info(dispatch_queue_attr_t dqa)
    {
        dispatch_queue_attr_info_t dqai = { };
        
        // 串行队列传的是NULL,会直接返回一个空的结构体
        if (!dqa) return dqai;
    
        // 给并发队列做相关的赋值操作
        ......
        
        return dqai;
    }
    
    // 第一个参数固定是4  第二个参数串行队列为true、并发队列为false
    DISPATCH_ALWAYS_INLINE DISPATCH_CONST
    static inline dispatch_queue_global_t
    _dispatch_get_root_queue(dispatch_qos_t qos, bool overcommit)
    {
        // 4-1= 3
        // 2*3+0/1 = 6/7
        // 串行取到的是_dispatch_root_queues这个数组里面下标为7的元素
        // 并发取到的是下标为6的元素
        return &_dispatch_root_queues[2 * (qos - 1) + overcommit];
    }
    
    // 给队列赋值
    static inline dispatch_queue_class_t
    _dispatch_queue_init(dispatch_queue_class_t dqu, dispatch_queue_flags_t dqf,
            uint16_t width, uint64_t initial_state_bits)
    {
        uint64_t dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(width);
        dispatch_queue_t dq = dqu._dq;
    
        dispatch_assert((initial_state_bits & ~(DISPATCH_QUEUE_ROLE_MASK |
                DISPATCH_QUEUE_INACTIVE)) == 0);
    
        if (initial_state_bits & DISPATCH_QUEUE_INACTIVE) {
            dq_state |= DISPATCH_QUEUE_INACTIVE + DISPATCH_QUEUE_NEEDS_ACTIVATION;
            dq->do_ref_cnt += 2; // rdar://8181908 see _dispatch_lane_resume
            if (dx_metatype(dq) == _DISPATCH_SOURCE_TYPE) {
                dq->do_ref_cnt++; // released when DSF_DELETED is set
            }
        }
    
        dq_state |= (initial_state_bits & DISPATCH_QUEUE_ROLE_MASK);
        dq->do_next = DISPATCH_OBJECT_LISTLESS;
        dqf |= DQF_WIDTH(width);
        os_atomic_store2o(dq, dq_atomic_flags, dqf, relaxed);
        dq->dq_state = dq_state;
        dq->dq_serialnum =
                os_atomic_inc_orig(&_dispatch_queue_serial_numbers, relaxed);
        return dqu;
    }
    

    根据代码可以获取到并发队列和串行队列的相关数据如下:

    • 并发队列
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,DISPATCH_PRIORITY_FLAG_FALLBACK,
        .dq_label = "com.apple.root.default-qos",
        .dq_serialnum = 10,
    )
    
    • 串行队列
    _DISPATCH_ROOT_QUEUE_ENTRY(DEFAULT,
        DISPATCH_PRIORITY_FLAG_FALLBACK | DISPATCH_PRIORITY_FLAG_OVERCOMMIT,
        .dq_label = "com.apple.root.default-qos.overcommit",
        .dq_serialnum = 11,
    )
    

    这个结果和打印的结果是完全相同的,这样也就走了一遍创建的过程。

    同理,也可以根据打印的数据得到主队列的信息如下:

    struct dispatch_queue_static_s _dispatch_main_q = {
        DISPATCH_GLOBAL_OBJECT_HEADER(queue_main),
    #if !DISPATCH_USE_RESOLVERS
        .do_targetq = _dispatch_get_default_queue(true),
    #endif
        .dq_state = DISPATCH_QUEUE_STATE_INIT_VALUE(1) |
                DISPATCH_QUEUE_ROLE_BASE_ANON,
        .dq_label = "com.apple.main-thread",
        .dq_atomic_flags = DQF_THREAD_BOUND | DQF_WIDTH(1),
        .dq_serialnum = 1, 
    };
    

    看完了GCD队列的相关信息,我们再来看看的队列和任务的配合使用。

    GCD的使用

    GCD的使用主要是任务和队列的配合使用,我们知道了任务的执行分为同步和异步,队列又有串行和并发之分。

    1. 同步函数串行队列:

    image

    根据代码可以看出:

    • 所有的任务默认在主线程执行
    • 同步任务不具备开启新线程的能力,只能在当前线程执行任务
    • 串行队列中的任务只能一个接着一个按顺序执行
    • 如果存在耗时任务,会产生堵塞

    由于主队列也是一种串行队列,我们再来看看同步函数搭配主队列会如何执行:

    image

    我们发现程序在第一个同步任务的地方崩溃了,这是因为此处发生了死锁。那是因为我们在主线程中执行syncTaskMainQueue方法,即把syncTaskMainQueue任务放到了主队列中。当我们把“打印1”这个同步任务追加到主队列中,“打印1”需要等待syncTaskMainQueue的执行,而syncTaskMainQueue需要等待“打印1”执行完毕,才能接着执行。这就形成了死锁。

    那么我们在其他线程执行该syncTaskMainQueue会死锁吗?调用如下方法:

    [NSThread detachNewThreadSelector:@selector(syncTaskMainQueue) toTarget:self withObject:nil];
    

    程序正常执行,因为此时syncTaskMainQueue在其他线程执行,而我们的打印任务都在主线程执行。

    2. 同步函数并发队列

    image
    • 所有的任务默认在主线程执行
    • 虽然是并发任务,但是同步任务不具备开启新线程的能力,所以只能在当前线程执行任务
    • 任务一个接一个按顺序执行
    • 如果存在耗时任务,会产生堵塞

    可以得出结论,无论是串行队列还是并发队列,只要是同步任务,都不会开启新线程,只能在当前线程执行任务,而且任务是一个接一个按顺序执行的,并且如果存在耗时任务会发生堵塞。

    3. 异步函数串行队列

    image
    • 开启了一条新线程
    • 任务一个接着一个,按顺序执行

    4. 异步函数并发队列

    image
    • 开启其他线程执行任务
    • 任务异步执行,没有顺序,和CPU调度有关

    可以得出结论,执行异步任务的时候,如果是串行队列,只会开启一条新的线程,任务会在新线程中一个接一个按顺序执行,而且可能会发生堵塞;如果是并发队列,有多少任务就会创建多少新的线程,任务异步执行,和CPU调度有关,没有特定顺序。

    总结

    GCD的核心就是将任务添加到队列,并且指定函数执行任务。任务分为同步任务和异步任务,而队列又分为串行队列和并发队列。主队列dispatch_get_main_queue()是常见的串行队列,全局队列dispatch_get_global_queue(0, 0)是常见的并发队列。

    任务和队列的配合使用分为同步函数串行队列、同步函数并发队列、异步函数串行队列、异步函数并发队列。

    执行同步任务的时候,无论是串行队列还是并发队列,都不会开启新线程,只能在当前线程执行任务,而且任务是一个接一个按顺序执行的,并且如果存在耗时任务会发生堵塞。需要注意的是,在主队列中加入同步任务,可能会导致死锁。

    执行异步任务的时候,如果是串行队列,只会开启一条新的线程,任务会在新线程中一个接一个按顺序执行,而且可能会发生堵塞;如果是并发队列,有多少任务就会创建多少新的线程,任务异步执行,和CPU调度有关,没有特定顺序。

    相关文章

      网友评论

        本文标题:iOS-多线程(二)-GCD基础

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