深入学习多线程之Dispatch Queues

介绍iOS中非常常用的轻量级多线程实现:Dispatch Queues

Dispatch Queue

简介

最早接触iOS的多线程实现多半就是GCD(grand center dispatch),而Dispatch queue(调度队列)是CGD技术基于C API的体现。Dispatch Queue的最大优势就是轻量,短短几行代码就能完成。但轻量绝不意味着功能的轻量。操作队列提供的功能,调度队列其实大部分都能实现。

调度队列的创建与管理

调度队列是一种先进先出的数据结构。我们可以通过dispatch_get_current_queue方法进行调试或测试当前队列。

串行调度队列

串行队列按照添加顺序,同一时间指执行一个任务。当前执行的任务所在的线程(不同的任务可能不同)有调度队列管理。同步队列常常会以同步方式访问资源。
可以创建很多串行队列,每个队列相互之间是并行的。

1
2
dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL);

并行调度队列

并行队列可以并行执行多个任务,但任务启动的顺序是根据添加顺序决定。实际同时执行的任务数是有队列根据系统条件自行决定的。
可以获取全局的并行队列或者建立私有的并行队列

1
2
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t bQueue = dispatch_queue_create("com.example.MyQueue", DISPATCH_QUEUE_CONCURRENT);

系统提供4种优先级:

  • DISPATCH_QUEUE_PRIORITY_HIGH
  • DISPATCH_QUEUE_PRIORITY_DEFAULT
  • DISPATCH_QUEUE_PRIORITY_LOW
  • DISPATCH_QUEUE_PRIORITY_BACKGROUND

主调度队列

主调度队列是全局可访问串行队列,其内任务运行在应用的主线程。该队列运行在应用的run loop内。我们通过dispatch_get_main_queue方法来获取程序的主调度队列。

上下文数据管理

所有的调度对象都可以用调度对象关联到上下文数据。我们用dispatch_set_context和dispatch_get_context函数进行数据关联。系统不会使用这些数据,所以需要手工管理内存分配和释放,用dispatch_set_finalizer_f设置清理函数。

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
void myFinalizerFunction(void *context)
{
MyDataContext* theData = (MyDataContext*)context;

// 清理数据结构
myCleanUpDataContextFunction(theData);

// 释放空间
free(theData);
}

dispatch_queue_t createMyQueue()
{
MyDataContext* data = (MyDataContext*) malloc(sizeof(MyDataContext));
myInitializeDataContextFunction(data);

// 创建队列并设置上下文
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.CriticalTaskQueue", NULL);
if (serialQueue)
{
dispatch_set_context(serialQueue, data);
dispatch_set_finalizer_f(serialQueue, &myFinalizerFunction);
}

return serialQueue;
}

任务添加

调度队列支持同步或异步的添加任务,但建议尽可能使用异步添加。
同步任务的死锁:

1
2
3
4
5
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

});
});

异步提交block任务

1
2
3
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"task");
});

关于block的使用可以参考网址

任务的KVO事件

调度队列的任务跟自定义的操作一样需要手工创建KVO事件。

任务的暂停

可以通过dispatch_suspend暂停队列,通过dispatch_resume恢复队列。暂停时队列的暂停数加1,恢复时减1,当暂停数大于0时,队列暂停。
暂停只影响新任务的执行,已经执行的任务不会暂停。

任务的取消

调度队列没有提供任何取消任务的方法,但我们可以对调度队列进行封装来进行模拟。其实我们一般用到的NSOperation中的cancel方法也是无法真正取消操作的,只有自定义NSOperation中在main中处理isCancelled方法才能实现真正的任务取消。

任务的依赖

block嵌套

在block中调用另一个调度队列,适合单依赖链的情况。

通过串行队列

将有依赖关系的任务加入到同一个串行队列中可以保证任务按照顺序执行,适合单依赖链的情况。

dispatch group

dispatch group可以将多个任务添加到一个组中,在组中任务全部结束后可以触发一个新任务:

1
2
3
4
5
6
7
8
9
10
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_main_queue(), ^{
NSLog(@"task1");
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSLog(@"task2");
});
dispatch_group_notify(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
NSLog(@"run");
});

dispatch group可以实现跨队列的任务依赖设置。dispatch_group_notify一定要在依赖任务都添加进来之后再执行。

dispatch semaphore

信号量是为了解决有限资源的访问问题而使用的方法,我们可以利用其来进行任务的依赖设定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"run");
});

dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"task1");
dispatch_semaphore_signal(semaphore);
});


dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"task2");
dispatch_semaphore_signal(semaphore);
});

dispatch_semaphore_wait方法会在信号量数量为0时阻塞线程,当不为0时,将信号量数量减1。dispatch_semaphore_signal可以使信号量数量加1。

dispatch barrier

当barrier block进入到私有的并发队列的最前端准备被执行时,队列会等之前正在执行的block全部结束后,才会执行后续的block任务。

1
2
3
4
5
6
7
8
9
10
11
dispatch_queue_t queue = dispatch_queue_create("com.example.barrier", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
sleep(3);
NSLog(@"1 fin");
});
dispatch_barrier_async(queue, ^{
NSLog(@"barrier");
});
dispatch_async(queue, ^{
NSLog(@"2 fin");
});

最终的输出结果是:

1
2
3
1 fin
barrier
2 fin

注意 一定是私有并发队列!串行队列是因为用这个没有意义。那全局并发队列为什么不生效呢?原因在于,这个队列是全局使用的,程序中可能很多其他地方,甚至是你引用的库中都可能往里面添加了block。如果barrier生效了,岂不是让所有使用该队列的任务都被挡住了。所以系统做了限制,如果你对全局队列使用了barrier,它会假装不知道,当成普通的任务来处理。

文章目录
  1. 1. Dispatch Queue
    1. 1.1. 简介
    2. 1.2. 调度队列的创建与管理
      1. 1.2.1. 串行调度队列
      2. 1.2.2. 并行调度队列
      3. 1.2.3. 主调度队列
      4. 1.2.4. 上下文数据管理
    3. 1.3. 任务添加
      1. 1.3.1. 异步提交block任务
    4. 1.4. 任务的KVO事件
    5. 1.5. 任务的暂停
    6. 1.6. 任务的取消
    7. 1.7. 任务的依赖
      1. 1.7.1. block嵌套
      2. 1.7.2. 通过串行队列
      3. 1.7.3. dispatch group
      4. 1.7.4. dispatch semaphore
      5. 1.7.5. dispatch barrier
,