盒子
盒子
Posts List
  1. 前言
  2. GCD简介
  3. GCD优势:
  4. GCD相关概念
  5. GCD三种队列类型
  6. 创建和管理队列
  7. 总结:

iOS GCD(Grand Central Dispatch)的使用(一)

作者Talent•C
转载请注明出处

前言

最近公司在招人,经常会问面试者一些关于多线程的问题,提到多线程就不得不说一下强大酷炫的GCD,对于GCD我一直没有系统的梳理,趁着现在有点小时间,把GCD相关知识梳理一遍,给自己留下个学习笔录,也方便更多的人理解GCD。

GCD简介

Grand Central Dispatch (GCD) comprises language features, runtime libraries, and system enhancements that provide systemic, comprehensive improvements to the support for concurrent code execution on multicore hardware in macOS, iOS, watchOS, and tvOS.
The BSD subsystem, Core Foundation, and Cocoa APIs have all been extended to use these enhancements to help both the system and your application to run faster, more efficiently, and with improved responsiveness. Consider how difficult it is for a single application to use multiple cores effectively, let alone doing it on different computers with different numbers of computing cores or in an environment with multiple applications competing for those cores. GCD, operating at the system level, can better accommodate the needs of all running applications, matching them to the available system resources in a balanced fashion.
主要讲的是:
GCD 可以对MacOS,iOS,WatchOS和tvOS中的多核硬件的并发代码执行提供系统全面的改进,使系统和应用程序运行得更快,更高效,并且响应更快,能够充分调度CPU多核高效率工作,以均衡的方式将它们与可用的系统资源进行匹配。
摘自:Apple 开发者文档

GCD优势:

易用: GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱,而且因为基于block,它能极为简单得在不同代码作用域之间传递上下文。
灵活: GCD 具有在常见模式上(比如锁、单例),用更高性能的方法优化代码,而且 GCD 能提供更多的控制权力以及大量的底层函数。
性能: GCD 能自动根据系统负载来增减线程数量,这就减少了上下文切换以及增加了计算效率。

GCD相关概念

  • Dispatch Objects:
    GCD是纯C语言的,但它被组建成面向对象的风格。GCD对象被称为dispatch object, 所有的dispatch objects都是OC对象.,就如其他OC对象一样,当开启了ARC(automatic reference counting)时,dispatch objects的retain和release都会自动执行。而如果是MRC的话,dispatch objects会使用dispatch_retain和dispatch_release这两个方法来控制引用计数。

  • Serial & Concurrent:
    串行任务就是每次只有一个任务被执行,并发任务就是在同一时间可以有多个任务被执行。

  • Synchronous & Asynchronous:
    同步函数 意思是在完成了它预定的任务后才返回,在任务执行时会阻塞当前线程。而 异步函数 则是任务会完成但不会等它完成,所以异步函数不会阻塞当前线程,会继续去执行下一个函数。

  • Context Switch:
    Context Switch即上下文切换,一个上下文切换指当你在单个进程里切换执行不同的线程时存储与恢复执行状态的过程。这个过程在编写多任务应用时很普遍,但会带来一些额外的开销。

  • Dispatch Queues:
    GCD dispatch queues是一个强大的执行多任务的工具。Dispatch queue是一个对象,它可以接受任务,并将任务以先进先出(FIFO)的顺序来执行。Dispatch queue可以并发的或串行的执行任意一个代码块,而且并发任务会像NSOperationQueue那样基于系统负载来合适地并发进行,串行队列同一时间则只执行单一任务。Dispatch queues内部使用的是线程,GCD 管理这些线程,并且使用Dispatch queues的时候,我们都不需要自己创建线程。Dispatch queues相对于和线程直接通信的代码优势是:Dispatch queues使用起来特别方便,执行任务更加有效率。

GCD三种队列类型

(1) 主线程队列: main queue可以调用dispatch_get_main_queue()来获得。因为main queue是与主线程相关的,所以这是一个串行队列。和其它串行队列一样,这个队列中的任务一次只能执行一个。它能保证所有的任务都在主线程执行,而主线程是唯一可用于更新 UI 的线程。

(2) 并发队列: 并发队列虽然是能同时执行多个任务,但这些任务仍然是按照先到先执行(FIFO)的顺序来执行的。并发队列会基于系统负载来合适地选择并发执行这些任务。在iOS5之前,并发队列一般指的就是全局队列(Global queue),进程中存在四个全局队列:高、中(默认)、低、后台四个优先级队列,可以调用dispatch_get_global_queue函数传入优先级来访问队列。而在iOS5之后,我们也可以用dispatch_queue_create,并指定队列类型DISPATCH_QUEUE_CONCURRENT,来自己创建一个并发队列。

(3) 串行队列: 串行队列将任务以先进先出(FIFO)的顺序来执行,所以串行队列经常用来做访问某些特定资源的同步处理。你可以也根据需要创建多个队列,而这些队列相对其他队列都是并发执行的。换句话说,如果你创建了4个串行队列,每一个队列在同一时间都只执行一个任务,对这四个任务来说,他们是相互独立且并发执行的。如果需要创建串行队列,一般用dispatch_queue_create这个方法来实现。

介绍完基本概念,我们看看如何使用…..


创建和管理队列

(1) global queue(全局队列):
当我们需要同时执行多个任务时,并发队列是非常有用的。并发队列其实仍然还是一个队列,它保留了队列中的任务按先进先出(FIFO)的顺序执行的特点。一个并发队列中实际执行的任务数是由很多因素决定的,比如系统的内核数,其他串行队列中任务的优先级,以及其他进程的工作状态。但是global queue 对于 dispatch_suspend(暂停)、dispatch_resume(恢复)、dispatch_set_context(切换上下文)函数无响应

我们来看一看 dispatch queue队列的优先级都有哪些

#define DISPATCH_QUEUE_PRIORITY_HIGH 2 //高

#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 //默认

#define DISPATCH_QUEUE_PRIORITY_LOW (-2) //低

#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN //后台

global queue 通过 dispatch_get_global_queue函数 获取

1
2
3
4
__OSX_AVAILABLE_STARTING(_MAC_10_6,__IPHONE_4_0)
DISPATCH_EXPORT DISPATCH_CONST DISPATCH_WARN_RESULT DISPATCH_NOTHROW
dispatch_queue_t
dispatch_get_global_queue(long identifier, unsigned long flags);

使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/*
* identifier: 队列的优先级 是个枚举值 后面会介绍每个枚举值的含义 DISPATCH_QUEUE_PRIORITY_DEFAULT(或者直接传入0)
* flags: 此参数为苹果预留参数,目前此参数传入0即可 据官方文档介绍如果此字段为非0可能会导致返回值为NULL
*/
dispatch_queue_t glQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//我们在这个全局并发队列中下载个图片 并在最后打印图片尺寸
/*
*创建异步线程
*/
dispatch_async(glQueue, ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg"]];
UIImage *img = [UIImage imageWithData:imageData];
NSLog(@"下载的图片尺寸: %@",NSStringFromCGSize(img.size));
});

运行结果:
2017-03-27 14:30:44.057 GCD-Test[94341:5165844] 下载的图片尺寸: {1024, 768}

到这里会发现现在所有的都是在子线程运行的,我们需要把图片赋值给UIImageView,所以我们必须回到主线程去更新UI,现在将上面的代码改造一下

1
2
3
4
5
6
7
8
dispatch_async(glQueue, ^{
NSData *imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@"http://pic33.nipic.com/20130916/3420027_192919547000_2.jpg"]];
UIImage *img = [UIImage imageWithData:imageData];
//将下载的数据转换成图片后回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"下载的图片尺寸: %@",NSStringFromCGSize(img.size));
});
});

注意:
尽管dispatch queues是引用计数对象,但是我们不需要用retain和release来管理全局的并发队列。因为全局队列对于程序来说是全局的,retain和release会被全局队列忽略,而且在ARC下这两个方法也会被忽略的。所以,我们不需要存储这些队列的引用数,仅仅只需要在任何要使用它们的地方,调用dispatch_get_global_queue这个方法即可。

(2) 并发队列和串行队列
当我们需要某些任务以指定的顺序去执行时,串行队列是一个非常好的选择。一个串行队列在同一时间里只会执行一个任务,而且每次都只会从队列的头部把任务取出来执行。正因为如此,我们可以用串行队列来替代锁的操作,比如数据资源的同步或修改数据结构时。和锁不同的是,串行队列能保证任务都是在可预见的顺序里执行,而且一旦我们在一个串行队列里异步提交了任务,队列就能永远不发生死锁。怎么样,是不是很棒,不过不像并发队列,这些串行队列是需要我们自己创建和管理的。

我们还可以在程序里创建任意数量的队列,不过值得注意的是,我们要尽量避免创建大量的串行队列而目的仅仅是为了同时执行队列中的这些任务。虽然GCD 通过创建所谓的线程池来大致匹配 CPU 内核数量,但是线程的创建并不是无代价的。每个线程都需要占用内存和内核资源。所以如果需要创建大量的并发任务,我们只需要把这些任务放到并发队列中即可。

代码示例

1
2
3
4
5
6
7
8
9
/**
* 创建串行队列
* 第一个参数 tanlentC.com 队列的标识 用户自定义标识字符串
* 第二个参数 队列属性 DISPATCH_QUEUE_SERIAL 串行队列 DISPATCH_QUEUE_CONCURRENT 并行队列 此参数为null 时默认串行队列
*/
dispatch_queue_t serialQueue = dispatch_queue_create("tanlentC.com.serialQueue", NULL);
//创建并行队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("tanlentC.com.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

下面是一些GCD的简单用法

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 后台执行:
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// something
});
//主线程执行:
dispatch_async(dispatch_get_main_queue(), ^{
// something
});
//一次性执行:
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// code to be executed once
});
// 延迟2秒执行:
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
// code to be executed on the main queue after delay
});
// 自定义dispatch_queue_t
dispatch_queue_t urls_queue = dispatch_queue_create("blog.devtang.com", NULL);
dispatch_async(urls_queue, ^{
   // your code
});
dispatch_release(urls_queue);
// 合并汇总结果
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行执行的线程一
});
dispatch_group_async(group, dispatch_get_global_queue(0,0), ^{
// 并行执行的线程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0,0), ^{
// 汇总结果
});
//定时器
__block int count_t = 0;
dispatch_queue_t queue = dispatch_get_main_queue(); //这里在非主线程队列运行
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
uint64_t interval = (uint64_t)(0.01 * NSEC_PER_SEC);
dispatch_source_set_timer(timer, start, interval, 0);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"定时器运行了 %d 次",count_t);
if (count_t == 100) {
//取消定时器
dispatch_cancel(timer);
}
count_t++;
});
// 启动定时器
dispatch_resume(timer);

关于GCD最基础的常用方法介绍完毕,下一篇文章会介绍较为深层次的用法

总结:

这篇文章整理的技术比较基础,适合初学者参考,基本都是概念性的东西,下一篇文章会主要以实践为主.

支持一下
扫一扫,支持Talent•C