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

简介

什么是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基础)