Objective-C高级编程(下):GCD

《Objective-C高级编程:iOS与OS X多线程和内存管理》是iOS开发中一本经典书籍,书中有关ARC、Block、GCD的梳理是iOS开发进阶路上必不可少的知识储备。笔者读完此书后为了加强理解,特以笔记记之。本文为完结篇,主要谈论Objective-C中的GCD。

Block

鉴于本书翻译自日文原版且翻译偏向书面,笔者希望采用通俗的语言记录,文章结构略有调整。

GCD概要

多线程编程

想聊GCD得先从多线程说起。

计算机执行应用程序时,先将代码转换成CPU命令列(二进制代码),再将包含在应用程序中的CPU命令列配置到内存中,CPU从应用程序指定的地址开始,一个一个地执行CPU命令列。由于一个CPU核一次只能执行一个CPU命令,不能执行某处分开的并列的两个命令,因此命令列就好比一条无分叉的大道。

  • 线程:一个CPU执行的命令列为一条无分叉路径”即为“线程”
  • 多线程:随着技术进步,一台计算机可以使用多个CPU核,存在多条这种无分叉路径时即为“多线程”

使用多线程的程序可以在某个线程和其他线程之间反复多次进行上下文切换,看上去好像一个CPU核能够并列执行多个线程一样。而在具有多个CPU核的情况下,真的提供多个CPU核并发执行多个线程的技术,称之为“多线程编程”

多线程编程容易发生的一些问题:

  • 数据竞争:多个线程更新相同的资源会导致数据的不一致。
  • 死锁:多个线程互相持续等待。
  • 内存消耗过大:使用太多线程会消耗大量内存。

多线程编程的问题

尽管极易发生各种问题,也应当使用多线程编程,因为多线程编程可保证应用程序的响应性能

主线程与其他线程

  • 主线程:启动应用最先执行的线程。用来描绘用户界面,处理触摸屏幕的事件等。
  • 其他线程:用来处理耗时操作。例如AR画像识别或数据库访问会阻塞主线程,妨碍主线程RunLoop的执行,从而导致不能更新用户界面、应用画面长时间停滞等问题。应在其他线程执行。

进程与线程

  • 进程:系统进行资源分配和调度的基本单位。每个程序的进程相互独立,有着各自的内存空间。
  • 线程:程序执行流的最小单元,如上述提到的命令列。

什么是GCD

GCD(Grand Central Dispatch)是异步执行任务的技术之一(iOS中其他多线程技术:pthread、NSThread、NSOperation)。开发者只需要定义想要执行的任务并追加到适当的Dispatch Quue中,GCD就能生成必要的线程并执行。

GCD用基于C语言的API,以非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,通过GCD提供的系统级线程管理可以提高执行效率

GCD队列

Dispatch Queue:执行处理的等待队列。

串行队列与并发队列

Dispatch Queue的种类:

种类 名称 说明 线程
Serial Dispatch Queue 串行队列 等待现在执行中处理结束 使用一个线程
Concurrent Dispatch Queue 并发队列 不等待现在执行中处理结束 使用多个线程
  • 串行队列同时执行的处理数只有一个,按照顺序执行。
  • 并发队列执的行顺序会取决于处理的任务量和系统的状态(CPU核数、CPU负荷等)。
  • 多个串行队列可并发执行,每个串行队列都使用各自的一个线程。

并行与并发

并行是并发的子集。通过调度算法实现逻辑上的同步执行属于并发,当多核CPU实现物理上的同步执行才是并行。

队列与线程的关系

队列和线程并非拥有关系,队列是任务容器(一种数据结构),CPU从队列中取出任务,放到对应的线程上去执行。

队列的创建

队列的创建使用dispatch_queue_create函数。

1
2
3
4
5
/**
* param1 字符串标识
* param2 队列类型
*/
dispatch_queue_t queue = dispatch_queue_create("com.example.queue", NULL);
1
2
3
4
5
// 串行队列
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

// 并发队列
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

当生成多个串行队列时,各个串行队列将并发执行。一旦生成串行队列并追加任务处理,系统对于一个串行队列就只使用一个线程。如果使用过多线程,就会消耗大量内存,引起大量的上下文切换,大幅度降低系统的响应性能。

为了避免使用多线程时出现数据竞争的问题,多个线程更新相同资源时可使用串行队列。

并发队列不会出现以上问题,不管生成多少,XNU内核只使用有效管理的线程。

生成的队列必须由程序员负责释放,因为队列并没有像Block那样具有作为Objective-C对象来处理的技术。释放使用dispatch_release()方法,与之对应有dispatch_retain()方法。

在iOS6.0、macOS 10.8及以上系统中,ARC已经能够管理GCD对象了,不应该使用dispatch_release()dispatch_retain()

主队列与全局队列

除了自行创建外,还可以获取系统标准提供的两种队列。

种类 名称 所属队列 工作线程
Main Dispatch Queue 主队列 串行队列 主线程
Global Dispatch Queue 全局队列 并发队列 其他线程
  • 追加到主队列的任务在主线程的RunLoop中执行,如更新用户界面的处理必须追加到主队列中,与NSObject类的performSelectorOnMainThread实例方法相同。
  • 全局队列无需逐个创建并发队列,只要获取使用即可。全局队列有四个优先级,优先级只是大致判断,并不能保证线程的实时性。
优先级 创建参数 说明
High Priority DISPATCH_QUEUE_PRIORITY_HIGH 最高优先级
Default Priority DISPATCH_QUEUE_PRIORITY_DEFAULT 默认优先级
Low Priority DISPATCH_QUEUE_PRIORITY_LOW 低优先级
Background Priority DISPATCH_QUEUE_PRIORITY_BACKGROUND 后台优先级
1
2
3
4
5
// 主队列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

// 全局队列
dispatch_queue_t globalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

对主队列和全局队列执行dispatch_releasedispatch_retain函数不会引起任何变化,也不会有任何问题。

dispatch_set_target_queue

变更队列优先级

dispatch_queue_create函数生成的串行队列和并发队列与默认优先级的全局队列使用相同优先级执行的线程。dispatch_set_target_queue可以变更生成的队列的优先级。

1
2
3
4
5
6
7
8
dispatch_queue_t myQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t backgroundQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);

/**
* param1 要变更的队列
* param2 目标队列
*/
dispatch_set_target_queue(myQueue, backgroundQueue);

防止多个串行队列并发执行

GCD中常使用dispatch_async函数d非同步(异步,不等待)追加任务到队列中执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)dispatch_queue_test_1 {

NSMutableArray *array = [NSMutableArray array];

// 5个串行队列
for (int i = 0; i < 5; i ++) {

dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);
[array addObject:serialQueue];
}

[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {

dispatch_async(queue, ^{

NSLog(@"执行任务:%ld",idx);
});
}];
}

输出:

1
2
3
4
5
执行队列:2
执行队列:1
执行队列:4
执行队列:0
执行队列:3

前面提到多个串行队列可并发执行,以上就是并发执行的结果。 使用dispatch_set_target_queue函数可以防止多个串行队列并发执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)dispatch_queue_test_2 {

NSMutableArray *array = [NSMutableArray array];

// 设置目标队列
dispatch_queue_t targetQueue = dispatch_queue_create("com.target.serialQueue", DISPATCH_QUEUE_SERIAL);

for (int i = 0; i < 5; i ++) {
dispatch_queue_t serialQueue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

// 给每个串行队列指定相同的目标队列
dispatch_set_target_queue(serialQueue, targetQueue);
[array addObject:serialQueue];
}

[array enumerateObjectsUsingBlock:^(dispatch_queue_t queue, NSUInteger idx, BOOL * _Nonnull stop) {

dispatch_async(queue, ^{

NSLog(@"执行队列:%ld",idx);
});
}];
}

输出:

1
2
3
4
5
执行队列:0
执行队列:1
执行队列:2
执行队列:3
执行队列:4

GCD函数

dispatch_after

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* param1 开始时间
* param2 持续时间
*/
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC));
dispatch_queue_t queue = dispatch_get_main_queue();

/**
* param1 时间
* param2 队列
*/
dispatch_after(time, queue, ^{

NSLog(@"等待3秒");
});

dispatch_after函数是在指定时间追加任务到指定队列中,并不是在指定时间后执行任务。想大致延迟任务时,该函数非常有效。

Dispatch Group

Dispatch Group适用于多个任务执行结束后,再执行某个指定的任务。创建任务组使用dispatch_group_create函数,追加任务使用dispatch_group_async函数。

  • dispatch_group_notify函数。起到监听的作用,当group中任务完成时可以做一些操作。
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
- (void)dispatch_notify {

// 组
dispatch_group_t group = dispatch_group_create();

// 队列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

// 5个任务
for (NSInteger index = 0; index < 5; index ++) {

/**
* param1 组
* param2 队列
*/
dispatch_group_async(group, queue, ^{

NSLog(@"任务%ld", index);
});
}

/**
* 监听任务的完成
* param1 组
* param2 队列
*/
dispatch_group_notify(group, queue, ^{

NSLog(@"任务完成");
});
}

输出:

1
2
3
4
5
任务1
任务2
任务0
任务3
任务完成
  • dispatch_group_wait函数。可以指定gropu任务超时的时间,无论指定的超时时间和group中任务完成哪个先到,dispatch_group_wait函数都会执行并有返回值。返回值为0即指定时间内任务全部完成,不为0则已超时,任务继续。
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)dispatch_wait {

dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

for (NSInteger index = 0; index < 5; index ++) {

dispatch_group_async(group, queue, ^{

for (NSInteger i = 0; i < 100000000; i ++) {

}
NSLog(@"任务%ld", index);
});
}

// 指定超时时间为2秒
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC));
long result = dispatch_group_wait(group, time);

if (result == 0) {
NSLog(@"未超时,任务已经完成");
}else {
NSLog(@"已超时,任务仍在继续");
}
}

输出:

1
2
3
4
5
6
任务3
任务1
任务0
任务2
任务4
未超时,任务已经完成

将上例中超时时间设置为0.2或更小值,输出:

1
2
3
4
5
6
已超时,任务仍在继续
任务1
任务3
任务2
任务0
任务4

dispatch_group_wait指定超时时间或 group任务完成之前,执行dispatch_group_wait函数的当前线程阻塞。推荐使用dispatch_group_notify函数追加结束任务到队列中,因为 dispatch_group_notify函数可以简化源代码。

dispatch_barrier_async

避免数据竞争的思路:在写入处理结束之前,读取处理不可执行,写入处理追加到串行队列中,为了提高效率,读取处理追加到并发队列中。

GCD 提供更高效的方法:dispatch_barrier_async函数

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
- (void)dispatch_barrier {

void (^blk_reading) (void) = ^{

for (NSInteger i = 0; i < 10000; i ++) {

}
NSLog(@"读取操作");
};

void (^blk_writing) (void) = ^{

for (NSInteger i = 0; i < 10000; i ++) {

}
NSLog(@"写入操作");
};

dispatch_queue_t queue = dispatch_queue_create("com.example.concurrentQueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, blk_reading);
dispatch_async(queue, blk_reading);
dispatch_async(queue, blk_reading);
dispatch_barrier_async(queue, blk_writing);
dispatch_async(queue, blk_reading);
dispatch_async(queue, blk_reading);
dispatch_async(queue, blk_reading);
}

输出:

1
2
3
4
5
6
7
读取操作
读取操作
读取操作
写入操作
读取操作
读取操作
读取操作

dispatch_barrier_async函数如同栅栏一般,使用并发队列和dispatch_barrier_async函数可实现高效率的数据访问和文件访问。

dispatch_sync

同步与异步

  • 同步(sync):任务完成前一直等待。
  • 异步(async):不做任何等待。

dispatch_group_wait相似,dispatch_sync的“等待”意味着阻塞当前线程,也可以说是简易版的dispatch_group_wait

适用于在主线程中使用其他线程执行任务,任务结束后使用所得到的结果。

1
2
3
4
5
6
7
8
9
- (void)dispatch_sync_0 {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_sync(queue, ^{

NSLog(@"同步处理");
});
}

注意:由于dispatch_sync会阻塞当前线程,使用不当会引起死锁。

1
2
3
4
5
6
7
8
9
- (void)dispatch_sync_1 {

dispatch_queue_t queue = dispatch_get_main_queue();

dispatch_sync(queue, ^{

NSLog(@"同步处理");
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)dispatch_sync_2 {

// 每个串行队列都会对应一个线程
dispatch_queue_t queue = dispatch_queue_create("com.example.serialQueue", DISPATCH_QUEUE_SERIAL);

dispatch_async(queue, ^{

dispatch_sync(queue, ^{

NSLog(@"同步处理");
});
});
}

以上两例都会发生死锁情况。

对死锁的理解

一个或多个线程中出现相互等待的情况就会发生死锁。死锁通常是双向阻塞导致的,具体到使用 GCD 开发中,当dispatch_sync和它追加的Block处于同一串行队列时,一定会发生死锁。因为 dispatch_sync会阻塞当前线程,等待Block执行完才会返回,而Block又得等待dispatch_sync执行完才会执行。

dispatch_apply

dispatch_apply函数按照指定的次数将Block任务追加到指定的队列中,等待任务完成再执行其他操作。与 dispatch_sync一样,dispatch_apply也会阻塞线程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)dispatch_apply_1 {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

/**
* param1 次数
* param2 指定的队列
* param3 带参数的Block
*/
dispatch_apply(10, queue, ^(size_t index) {

NSLog(@"任务%zu完成", index);
});

NSLog(@"全部完成");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)dispatch_apply_2 {

NSArray *array = @[@"任务1", @"任务2", @"任务3", @"任务4", @"任务5", @"任务6", @"任务7", @"任务8"];

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

dispatch_async(queue, ^{

dispatch_apply([array count], queue, ^(size_t index) {

// 处理任务
NSLog(@"%@", [array objectAtIndex:index]);
});

dispatch_async(dispatch_get_main_queue(), ^{

NSLog(@"任务完成,更新UI");
});
});
}

dispatch_suspend / dispatch_resume

1
2
3
4
5
// 挂起指定队列
dispatch_suspend(queue);

// 恢复指定队列
dispatch_resume(queue);

这些操作不影响已经执行的任务。挂起后,队列中未执行的任务会停止,恢复后这些任务会继续执行。

Dispatch Semaphore

Dispatch Semaphore是持有计数的信号,该信号是多线程编程中的计数类型信号。信号类似于过马路时的手旗,可以通过时举起手旗,不可通过时放下手旗。

Dispatch Semaphore常用以下三个函数:

函数名 参数 说明
dispatch_semaphore_create 1个参数:大于等于0的数值 创建信号
dispatch_semaphore_signal 1个参数:信号(semaphore) 信号量 +1
dispatch_semaphore_wait 2个参数:信号(semaphore)和时间 信号量 -1,若为0则等待
  • 信号量用于对资源进行加锁操作。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)dispatch_semaphore_1 {

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1);

NSMutableArray *array = [[NSMutableArray alloc] init];

for (NSInteger i = 0; i < 10000; i ++) {

dispatch_async(queue, ^{

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

// 模拟数据写入操作
[array addObject:@(i)];

dispatch_semaphore_signal(semaphore);
});
}
}
  • 信号量用于链式请求,限制一个请求完成后再去执行下一个。
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
- (void)dispatch_semaphore_2 {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSArray *array = @[@"1", @"2", @"3", @"4", @"5"];

// 初始化信号量为0
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {

[self requestWithCompletion:^(NSDictionary *dict) {

NSLog(@"%@-%@", dict[@"message"], obj);

dispatch_semaphore_signal(semaphore);
}];

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}];
});
}

- (void)requestWithCompletion:(void(^)(NSDictionary *dict))completion {

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

//模拟网络请求
sleep(2);
!completion ? nil : completion(@{@"message":@"任务完成"});
});
}

dispatch_once

dispatch_once函数能保证应用程序中任务只执行一次,该代码在多线程环境下执行可保证百分之百安全。常用于生成单例。

1
2
3
4
5
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 执行一次的任务
});
1
2
3
4
5
6
7
8
9
10
11
12
13
// 单例
+ (instancetype)sharedInstance {

static Class *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{

// 初始化
sharedInstance = [[Class alloc] init];

return shardInstance;
});
}

以上就是GCD部分的学习内容。

您的支持将鼓励我继续创作!