GCD学习

重新复习GCD

对GCD重新复习一次,加深了解

多线程概念

进程

  • 正在进行中的程序被称为进程,负责程序运行的内存分配
  • 每一个进程都有自己独立的虚拟内存空间

线程

  • 线程是进程中一个独立的执行路径(控制单元)
  • 一个进程中至少包含一条线程,即主线程
  • 可以将耗时的执行路径(如:网络请求)放在其他线程中执行

细说GCD(Grand Central Dispatch)

GCD(Grand Central Dispatch) 介绍

GCD属于系统级的线程管理,在Dispatch queue中执行需要执行的任务性能非常的高。GCD这块已经开源,地址 http://libdispatch.macosforge.org .GCD中的FIFO队列称为dispatch queue,用来保证先进来的任务先得到执行。

  • GCD 能通过推迟昂贵计算任务并在后台运行它们来改善你的应用的响应性能。
  • GCD 提供一个易于使用的并发模型而不仅仅只是锁和线程,以帮助我们避开并发陷阱。
  • GCD 具有在常见模式(例如单例)上用更高性能的原语优化你的代码的潜在能力。

GCD概要

  • 和operation queue一样都是基于队列的并发编程API,他们通过集中管理大家协同使用的线程池。

  • 公开的5个不同队列:运行在主线程中的main queue,3个不同优先级的后台队列(High Priority Queue,Default Priority Queue,Low Priority Queue),以及一个优先级更低的后台队列Background Priority Queue(用于I/O)

  • 可创建自定义队列:串行或并列队列。自定义一般放在Default Priority Queue和Main Queue里。

  • 操作是在多线程上还是单线程主要是看队列的类型和执行方法,并行队列异步执行才能在多线程,并行队列同步执行就只会在这个并行队列在队列中被分配的那个线程执行。

GCD基本思想

GCD的基本思想是就将操作s放在队列s中去执行

  • 操作使用Blocks定义
  • 队列负责调度任务执行所在的线程以及具体的执行时间
  • 队列的特点是先进先出(FIFO)的,新添加至对列的操作都会排在队尾
队列 (dispatch_queue_t)
  • 串行队列,队列中的任务只会顺序执行
  • 并行队列,队列中的任务通常会并发执行
操作
  • dispatch_async 异步操作,会并发执行,无法确定任务的执行顺序
  • dispatch_sync 同步操作,会依次顺序执行,能够决定任务的执行顺序
注:
  • 队列不是线程.也不表示对应的CPU
  • 队列就是负责调度的!谁空闲,就把任务给谁!
  • 多线程技术的目的,就是为了在一个CPU上实现快速切换!
并发队列 串行队列(非主队列) 主队列(只有主线程,串行队列)
同步(dispatch_sync) 不开启新的线程,串行 不开启新的线程,串行 不开启新的线程,串行
异步(dispatch_async) 开启新的线程,并发 开启新的线程,串行 不开启新的线程,串行

GCD 术语

Serial vs. Concurrent 串行 vs. 并发

描述当前任务相对于其它任务被执行,串行执行就是每次只有一个任务被执行,并发执行就是在同一时间可以有多个任务被执行。

Synchronous vs. Asynchronous 同步 vs. 异步

一个_同步_函数 dispatch_sync 只在完成了它预定的任务后才返回。把一个任务添加到某queue后,等这个任务完成,调用线程才继续执行.

一个_异步_函数 dispatch_async ,刚好相反,会立即返回,预定的任务会完成但不会等它完成。因此,一个异步函数不会阻塞当前线程去执行下一个函数。 把一个任务添加到某queue后就马上离开,而不管任务在那个queue里的执行状态

基本概念

  • 系统标准两个队列
1
2
3
4
//全局队列,一个并行的队列
dispatch_get_global_queue
//主队列,主线程中的唯一队列,一个串行队列
dispatch_get_main_queue
  • 自定义队列
1
2
3
4
//串行队列
dispatch_queue_create("com.sylar.serialqueue", DISPATCH_QUEUE_SERIAL)
//并行队列
dispatch_queue_create("com.sylar.concurrentqueue",DISPATCH_QUEUE_CONCURRENT)
  • 同步异步线程创建
1
2
3
4
//同步线程
dispatch_sync(..., ^(block))
//异步线程
dispatch_async(..., ^(block))

队列(dispatch queue)

  • Serial:又叫private dispatch queues,同时只执行一个任务。Serial queue常用于同步访问特定的资源或数据。当你创建多个Serial queue时,虽然各自是同步,但serial queue之间是并发执行。
  • Main dispatch queue:全局可用的serial queue,在应用程序主线程上执行任务。
  • Concurrent:又叫global dispatch queue,可以并发的执行多个任务,但执行完成顺序是随机的。系统提供四个全局并发队列,这四个队列有这对应的优先级,用户是不能够创建全局队列的,只能获取。
1
2
dipatch_queue_t queue;
queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH,0);
  • User create queue:创建自己定义的队列,可以用dispatch_queue_create函数,函数有两个参数,第一个自定义的队列名,第二个参数是队列类型,默认NULL或者DISPATCH_QUEUE_SERIAL的是串行,参数为DISPATCH_QUEUE_CONCURRENT为并行队列。
1
2
dispatch_queue_t queue
queue = dispatch_queue_create("com.sylar.gcddemo.concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
  • 自定义队列的优先级:可以通过dipatch_queue_attr_make_with_qos_class或dispatch_set_target_queue方法设置队列的优先级
1
2
3
4
5
6
7
8
//dipatch_queue_attr_make_with_qos_class
dispatch_queue_attr_t attr = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_UTILITY, -1);
dispatch_queue_t queue = dispatch_queue_create("com.sylar.gcddemo.qosqueue", attr);

//dispatch_set_target_queue
dispatch_queue_t queue = dispatch_queue_create("com.sylar.gcddemo.settargetqueue",NULL); //需要设置优先级的queue
dispatch_queue_t referQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); //参考优先级
dispatch_set_target_queue(queue, referQueue); //设置queue和referQueue的优先级一样
  • 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
24
25
dispatch_queue_t serialQueue = dispatch_queue_create("com.sylar.gcddemo.serialqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t firstQueue = dispatch_queue_create("com.sylar.gcddemo.firstqueue", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t secondQueue = dispatch_queue_create("com.sylar.gcddemo.secondqueue", DISPATCH_QUEUE_CONCURRENT);

dispatch_set_target_queue(firstQueue, serialQueue);
dispatch_set_target_queue(secondQueue, serialQueue);

dispatch_async(firstQueue, ^{
NSLog(@"1");
[NSThread sleepForTimeInterval:3.f];
});
dispatch_async(secondQueue, ^{
NSLog(@"2");
[NSThread sleepForTimeInterval:2.f];
});
dispatch_async(secondQueue, ^{
NSLog(@"3");
[NSThread sleepForTimeInterval:1.f];
});


2017-09-21 00:01:41.421 GCDDemo[1622:150923] 1
2017-09-21 00:01:44.427 GCDDemo[1622:150923] 2
2017-09-21 00:01:46.429 GCDDemo[1622:150923] 3
(设置为串行队列了)

队列类型

队列默认是串行的,如果设置改参数为NULL会按串行处理,只能执行一个单独的block,队列也可以是并行的,同一时间执行多个block

5种队列,主队列(main queue),四种通用调度队列,自己定制的队列。四种通用调度队列为

  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
  • QOS_CLASS_USER_INTERACTIVE:user interactive等级表示任务需要被立即执行提供好的体验,用来更新UI,响应事件等。这个等级最好保持小规模。
  • QOS_CLASS_USER_INITIATED:user initiated等级表示任务由UI发起异步执行。适用场景是需要及时结果同时又可以继续交互的时候。
  • QOS_CLASS_UTILITY:utility等级表示需要长时间运行的任务,伴有用户可见进度指示器。经常会用来做计算,I/O,网络,持续的数据填充等任务。这个任务节能。
  • QOS_CLASS_BACKGROUND:background等级表示用户不会察觉的任务,使用它来处理预加载,或者不需要用户交互和对时间不敏感的任务。

何时使用何种队列类型

  • 主队列(顺序):队列中有任务完成需要更新UI时,dispatch_after在这种类型中使用。
  • 并发队列:用来执行与UI无关的后台任务,dispatch_sync放在这里,方便等待任务完成进行后续处理或和dispatch barrier同步。dispatch groups放在这里也不错。
  • 自定义顺序队列:顺序执行后台任务并追踪它时。这样做同时只有一个任务在执行可以防止资源竞争。dipatch barriers解决读写锁问题的放在这里处理。dispatch groups也是放在这里。

dispatch_once用法

dispatch_once_t要是全局或static变量,保证dispatch_once_t只有一份实例

1
2
3
4
5
6
7
8
9
10
+ (UIColor *)boringColor;
{
static UIColor *color;
//只运行一次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
color = [UIColor colorWithRed:0.380f green:0.376f blue:0.376f alpha:1.000f];
});
return color;
}

dispatch_async

设计一个异步的API调用dispatch_async(),这个调用放在API的方法或函数中做。让API的使用者设置一个回调处理队列

1
2
3
4
5
6
7
8
9
10
- (void)processImage:(UIImage *)image completionHandler:(void(^)(BOOL success))handler;
{
dispatch_async(self.isolationQueue, ^(void){
// do actual processing here
dispatch_async(self.resultQueue, ^(void){
handler(YES);
});
});
}

可以避免界面会被一些耗时的操作卡死,比如读取网络数据,大数据IO,还有大量数据的数据库读写,这时需要在另一个线程中处理,然后通知主线程更新界面,GCD使用起来比NSThread和NSOperation方法要简单方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//代码框架
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 耗时的操作
dispatch_async(dispatch_get_main_queue(), ^{
// 更新界面
});
});

//下载图片的示例
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSURL * url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"];
NSData * data = [[NSData alloc]initWithContentsOfURL:url];
UIImage *image = [[UIImage alloc]initWithData:data];
if (data != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
});
}
});

dispatch_after延后执行

dispatch_after只是延时提交block,不是延时立刻执行。

1
2
3
4
5
6
7
8
- (void)foo
{
double delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t) (delayInSeconds * NSEC_PER_SEC));
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self bar];
});
}

dispatch_barrier_async使用Barrier Task方法Dispatch Barrier解决多线程并发读写同一个资源发生死锁

Dispatch Barrier确保提交的闭包是指定队列中在特定时段唯一在执行的一个。在所有先于Dispatch Barrier的任务都完成的情况下这个闭包才开始执行。轮到这个闭包时barrier会执行这个闭包并且确保队列在此过程不会执行其它任务。闭包完成后队列恢复。需要注意dispatch_barrier_async只在自己创建的队列上有这种作用,在全局并发队列和串行队列上,效果和dispatch_sync一样

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
- (void)dispatchBarrierAsyncDemo {
//防止文件读写冲突,可以创建一个串行队列,操作都在这个队列中进行,没有更新数据读用并行,写用串行。
dispatch_queue_t dataQueue = dispatch_queue_create("com.starming.gcddemo.dataqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(dataQueue, ^{
[NSThread sleepForTimeInterval:2.f];
NSLog(@"read data 1");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 2");
});
//等待前面的都完成,在执行barrier后面的
dispatch_barrier_async(dataQueue, ^{
NSLog(@"write data 1");
[NSThread sleepForTimeInterval:1];
});
dispatch_async(dataQueue, ^{
[NSThread sleepForTimeInterval:1.f];
NSLog(@"read data 3");
});
dispatch_async(dataQueue, ^{
NSLog(@"read data 4");
});
}

//2017-09-26 23:07:17.135287+0800 GCDDemo[68558:2264984] read data 2
//2017-09-26 23:07:19.138474+0800 GCDDemo[68558:2264982] read data 1
//2017-09-26 23:07:19.138680+0800 GCDDemo[68558:2264982] write data 1
//2017-09-26 23:07:20.138850+0800 GCDDemo[68558:2264981] read data 4
//2017-09-26 23:07:21.141004+0800 GCDDemo[68558:2264982] read data 3

dispatch_apply进行快速迭代

类似for循环,但是在并发队列的情况下dispatch_apply会并发执行block任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
for (size_t y = 0; y < height; ++y) {
for (size_t x = 0; x < width; ++x) {
// Do something with x and y here
}
}
//因为可以并行执行,所以使用dispatch_apply可以运行的更快
- (void)dispatchApplyDemo {
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_apply(10, concurrentQueue, ^(size_t i) {
NSLog(@"%zu",i);
});
NSLog(@"The end"); //这里有个需要注意的是,dispatch_apply这个是会阻塞主线程的。这个log打印会在dispatch_apply都结束后才开始执行
}

dispatch_apply能避免线程爆炸,因为GCD会管理并发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- (void)dealWiththreadWithMaybeExplode:(BOOL)explode {
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
if (explode) {
//有问题的情况,可能会死锁
for (int i = 0; i < 999 ; i++) {
dispatch_async(concurrentQueue, ^{
NSLog(@"wrong %d",i);
//do something hard
});
}
} else {
//会优化很多,能够利用GCD管理
dispatch_apply(999, concurrentQueue, ^(size_t i){
NSLog(@"correct %zu",i);
//do something hard
});
}
}

配合 dispatch_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
func downloadPhotosWithCompletion(completion: BatchPhotoDownloadingCompletionClosure?) {
var storedError: NSError!
var downloadGroup = dispatch_group_create()
let addresses = [OverlyAttachedGirlfriendURLString,
SuccessKidURLString,
LotsOfFacesURLString]

dispatch_apply(UInt(addresses.count), GlobalUserInitiatedQueue) {
i in
let index = Int(i)
let address = addresses[index]
let url = NSURL(string: address)
dispatch_group_enter(downloadGroup)
let photo = DownloadPhoto(url: url!) {
image, error in
if let error = error {
storedError = error
}
dispatch_group_leave(downloadGroup)
}
PhotoManager.sharedManager.addPhoto(photo)
}

dispatch_group_notify(downloadGroup, GlobalMainQueue) {
if let completion = completion {
completion(error: storedError)
}
}
}

Block组合Dispatch_groups

dispatch groups是专门用来监视多个异步任务。dispatch_group_t实例用来追踪不同队列中的不同任务。

当group里所有事件都完成GCD API有两种方式发送通知,第一种是dispatch_group_wait,会阻塞当前进程,等所有任务都完成或等待超时。第二种方法是使用dispatch_group_notify,异步执行闭包,不会阻塞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)dispatchGroupWaitDemo {
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
//在group中添加队列的block
dispatch_group_async(group, concurrentQueue, ^{
[NSThread sleepForTimeInterval:2.f];
NSLog(@"1");
});
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"2");
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"go on");
}


//2017-09-28 15:02:10.213870+0800 GCDDemo[9837:401007] 2
//2017-09-28 15:02:12.215037+0800 GCDDemo[9837:401004] 1
//2017-09-28 15:02:12.215219+0800 GCDDemo[9837:400753] can continue
//执行完group里面所有任务才继续
//串行卡在这里

dispatch_group_notify

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)dispatchGroupNotifyDemo {
dispatch_queue_t concurrentQueue = dispatch_queue_create("com.starming.gcddemo.concurrentqueue",DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"1");
});
dispatch_group_async(group, concurrentQueue, ^{
NSLog(@"2");
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"end");
});
NSLog(@"can continue");

}

//2017-09-28 15:07:45.379203+0800 GCDDemo[9929:411297] can continue
//2017-09-28 15:07:45.379203+0800 GCDDemo[9929:411654] 2
//2017-09-28 15:07:45.379203+0800 GCDDemo[9929:411652] 1
//2017-09-28 15:07:45.385121+0800 GCDDemo[9929:411297] end

//执行后代码会继续,所有任务完成后才会回调
//约等于 通知

注意事项:

  • dispatch_group_async等价于dispatch_group_enter() 和 dispatch_group_leave()的组合。
  • dispatch_group_enter() 必须运行在 dispatch_group_leave() 之前。
  • dispatch_group_enter() 和 dispatch_group_leave() 需要成对出现的

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
54
55
56
- (void)deadLockCase1 {
NSLog(@"1");
//主队列的同步线程,按照FIFO的原则(先入先出),2排在3后面会等3执行完,但因为同步线程,3又要等2执行完,相互等待成为死锁。
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
}
- (void)deadLockCase2 {
NSLog(@"1");
//3会等2,因为2在全局并行队列里,不需要等待3,这样2执行完回到主队列,3就开始执行
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSLog(@"2");
});
NSLog(@"3");
}
- (void)deadLockCase3 {
dispatch_queue_t serialQueue = dispatch_queue_create("com.starming.gcddemo.serialqueue", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
//串行队列里面同步一个串行队列就会死锁
dispatch_sync(serialQueue, ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
- (void)deadLockCase4 {
NSLog(@"1");
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"2");
//将同步的串行队列放到另外一个线程就能够解决
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"3");
});
NSLog(@"4");
});
NSLog(@"5");
}
- (void)deadLockCase5 {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
NSLog(@"1");
//回到主线程发现死循环后面就没法执行了
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"2");
});
NSLog(@"3");
});
NSLog(@"4");
//死循环
while (1) {
//
}
}

RunLoop关系

摘自 StackoverFlow

  1. The main thread’s run loop has a step in which it runs any blocks queued on the main queue.
    主线程的runloop有一个步骤,它在主队列上运行任何block。

  2. GCD creates the threads for concurrent queues. A thread doesn’t have a run loop until the first time something running on the thread asks for the thread’s run loop, at which point the system creates a run loop for the thread. However, the run loop only runs if something on that thread then asks it to run (by calling -[NSRunLoop run] or CFRunLoopRun or similar). Most threads, including threads created for GCD queues, never have a run loop.
    GCD 创建并发队列的线程,线程的Runloop只在第一次运行时候请求runloop才会创建,也就是说系统为线程创建Runloop。Runloop只是在有队列任务请求时候才会运行,大部分线程,包括GCD队列的线程,并不会存在Runloop

  3. GCD manages a pool of threads and, when it needs to run a block (because it was added to some queue), GCD picks the thread on which to run the block. GCD’s thread-choosing algorithm is mostly an implementation detail, except that it will always choose the main thread for a block that was added to the main queue. (Note that GCD will also sometimes use the main thread for a block added to some other queue.)

    GCD管理线程池,当需要执行一些Block,GCD会选择一个线程去运行Block,GCD的 线程选择算法已经能帮用户实现大部分需求,除了主动添加在主线程上的Block(注:GCD有时候会利用主线程,添加一些Block到其他线程上)

  4. You can only get the run loop of the main thread (using +[NSRunLoop mainRunLoop] or CFRunLoopGetMain) or the run loop of the current thread (using +[NSRunLoop currentRunLoop] or CFRunLoopGetCurrent). If you need the run loop of some arbitrary thread, you must find a way to call CFRunLoopGetCurrent on that thread and pass its return value back across threads in a safe, synchronized way.
    你只能获得主线程runloop,如果你需要获得任意线程的Runloop,你需要找个方法调用那个线程的 CFRunLoopGetCurrent,同步形式下获得他的返回值

总结:

  1. 一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的RunLoop对象。
  2. 我们只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。
  3. RunLoop对象在第一次获取RunLoop时创建,销毁则是在线程结束的时候。
  4. 主线程的RunLoop对象系统自动帮助我们创建好了(原理如下),而子线程的RunLoop对象需要我们主动创建。

队列无法销毁.只能挂起(NSOperation),同样,里面的线程也是挂起状态
dispatch_async 同步,创建的线程由系统决定,由队列管理.

Author

Sylar

Posted on

2019-04-28

Updated on

2021-11-14

Licensed under

Comments