GCD与多线程编程

时间:2022-04-23 05:15:47

1. 什么是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 iOS and OS X.

大致意思是说,GCD是异步执行任务的技术,提供语言特征、运行时库、系统全面的改进的多核硬件上的支持。开发者只需要定义想执行的任务并加入到队列中就可以了。

也就是说,GCD用我们难以置信的非常简洁的记述方法,实现了极为复杂繁琐的多线程编程,可以说是一项划时代的技术。

同时它具有很多优点:

  • 直观而简单的编程接口

  • 提供自动和整体的线程池管理

  • 提供汇编级调优的速度

  • 更加高效的使用内存

  • 不会trap内核

  • 异步分派任务到dispatch queue,不对导致queue死锁

  • 伸缩性强

  • serial dispatch queue比锁和其他同步原语更加高效

2 GCD APIs

2.1串行队列Serial Diapatch Queue
创建串行队列,对于第二个参数可为DISPATCH_QUEUE_SERIAL或者NULL,队列中操作会按顺序执行。

dispatch_queue_t myQueue = dispatch_queue_create("com.my.wangruih", DISPATCH_QUEUE_SERIAL);

dispatch_async(myQueue, ^{

NSLog(@"1");

});

dispatch_async(myQueue, ^{

NSLog(@"2");

});

dispatch_async(myQueue, ^{

NSLog(@"3");

});

以上语句的输出结果为:

2016-06-27 15:11:40.612 TestGCD[34781:4486699] 1

2016-06-27 15:11:40.613 TestGCD[34781:4486699] 2

2016-06-27 15:11:40.613 TestGCD[34781:4486699] 3

三者相互依赖,串行执行。

2.2并行队列Concurrent Diapatch Queue
往队列中追加的操作,没有相互依赖关系,执行会放到不同的线程中,执行的先后顺序未知。

dispatch_queue_t myQueue

= dispatch_queue_create("com.my.wangruih", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(myQueue, ^{

NSLog(@"1");

});

dispatch_async(myQueue, ^{

NSLog(@"2");

});

dispatch_async(myQueue, ^{

NSLog(@"3");

});

运行后发现日志为:

2016-06-27 15:18:23.895 TestGCD[34834:4493908] 2

2016-06-27 15:18:23.895 TestGCD[34834:4493899] 1

2016-06-27 15:18:23.895 TestGCD[34834:4493934] 3

当然这只是其中的一种情况,按照排列组合计算,可以产生6种不同的顺序。

如前所诉,concurrent dispatch queue并行执行多个处理,而serial只能执行1个追加处理。虽然一个serial queue只能执行一个,但是可以创建多个,代价便是产生多个线程,过多的线程会消耗大量内存,频繁的上下文切换会大幅降低性能。

2.3 Main Dispatch Queue/Global Dispatch Queue

在我们不打算自己生成dispatch queue的情况下,系统会为我们准备两个,那就是Main Dispatch Queue(就是serial queue),Global Dispatch Queue(就是concurrent queue)。

  • Main顾名思义就是主线程,所有的操作都会追加到主线程去执行,大多都是用户界面的更新。

  • Global是说有程序使用的concurrent queue,同时它具有四个优先级分别为High,Default,Low,Background。通过XNU内核根据优先级来调度线程执行。

3 说完了队列再来说说,操作队列的一些方法
3.1 dispath_set_target_queue
变更生成的dispatch queue的执行优先级使用dispatch_set_target_queue函数,在实际的工作里我倒是没用到过该方法。

dispatch_queue_t mySerialQueue = dispatch_queue_create("com.my.wangruih", DISPATCH_QUEUE_SERIAL);

dispatch_queue_t myGlobalQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

dispatch_set_target_queue(mySerialQueue, myGlobalQueue);

dispatch_async(mySerialQueue, ^{

NSLog(@"hello");

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@"world");

});

执行结果如下,根据结果看出该方法修改了优先级。

2016-06-27 15:46:05.972 TestGCD[34901:4517280] world

2016-06-27 15:46:05.973 TestGCD[34901:4517287] hello

3.2 dispatch_after
这个在工作中倒是常常用到,我常用在tableview reloaddata场景中,因为有一些诸如点赞动画之类的,加入该方法是的点赞动画执行完毕后的0.5~1s再执行reloadData操作。

dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{

NSLog(@"what happened");

});

// 输出

2016-06-27 15:51:07.921 TestGCD[34918:4521806] hello

2016-06-27 15:51:11.216 TestGCD[34918:4521762] what happened

大致该语句执行是在3秒之后,也就是3秒之后追加block到main queue。这个时间并不是绝对的,block只是追加到了main runloop中,而main queue可能有大量其他处理,会使得时间变长。第一个参数是指定的dispatch_time_t类型。该值有dispatch_time或dispatch_walltime。前者通常用于相对时间,后者为绝对时间。如希望在2016/11/11 -0:0执行。

dispatch_time_t getDispatchTimeByData(NSDate *date) {

NSTimeInterval interval;

double second, subsecond;

struct timespec time;

dispatch_time_t milestone;

interval = [date timeIntervalSince1970];

subsecond = modf(interval, &second);

time.tv_sec = second;

time.tv_nsec = subsecond * NSEC_PER_SEC;

milestone = dispatch_walltime(&time, 0);

return milestone;

}

NSDate *date = [[NSDate alloc] initWithTimeIntervalSinceNow:5];

dispatch_after(getDispatchTimeByData(date), dispatch_get_main_queue(), ^{

NSLog(@"happen sth");

});

// 输出

2016-06-27 16:04:06.949 TestGCD[34946:4531114] what happened

2016-06-27 16:04:09.439 TestGCD[34946:4531114] happen sth

3.3 dispatch_group
如果有3个操作(A,B,C)需要在并行执行完A,B之后再执行C操作,可以有多个实现方式,当然可以通过添加依赖关系addDependency来实现。通过dispatch_group的方式依然可以实现。

无论向什么样的dispatch queue中追加处理,使用dispatch group都能监视到所有处理的结束,就可以将结束的处理追加到dispatch queue中,这是使用dispatch group的原因。

dispatch_group_t group = dispatch_group_create();

dispatch_queue_t queue = dispatch_queue_create("com.my.wangruih", DISPATCH_QUEUE_CONCURRENT);

dispatch_group_async(group, queue, ^{

NSLog(@"A");

});

dispatch_group_async(group, queue, ^{

NSLog(@"B");

});

dispatch_group_notify(group, queue, ^{

NSLog(@"C");

});

// 输出

2016-06-28 09:07:18.261 TestGCD[35461:4595668] B

2016-06-28 09:07:18.261 TestGCD
[35461:4595678] A

2016-06-28 09:07:18.262 TestGCD
[35461:4595668] C

3.4 dispatch_barrier_async
在访问数据库或者文件时,有可能发生数据竞争。此方法的作用便是在并发队列中,完成在它之前提交到队列中的任务后打断,单独执行其block。起到了一个线程锁的作用。同样适用于上节中的,A,B,C的执行顺序问题。

dispatch_queue_t queue = dispatch_queue_create("com.my.wangruih", DISPATCH_QUEUE_CONCURRENT);

dispatch_async(queue, ^{

NSLog(@"A");

});

dispatch_async(queue, ^{

NSLog(@"B");

});

dispatch_barrier_async(queue, ^{

NSLog(@"C");

});

dispatch_async(queue, ^{

NSLog(@"D");

});

// 输出

2016-06-28 09:21:43.864 TestGCD[35515:4607006] A

2016-06-28 09:21:43.864 TestGCD[35515:4607015] B

2016-06-28 09:21:43.865 TestGCD[35515:4607015] C

2016-06-28 09:21:43.865 TestGCD[35515:4607015] D

3.5 dispatch_sync
它以为着事件是同步发生的,也就是指定的block同步追加到指定的dispatch queue中。在追加block结束之前,dispatch_sync函数会一直等待。这里有一个经常提及的问题来考察对同步的理解。

题目如下,输出结果是什么,为什么会这样。

NSLog(@"1");

dispatch_sync(dispatch_get_main_queue(), ^{

NSLog(@"2");

})
;

NSLog(@"3");

输出结果是:

2016-06-28 09:28:10.713 TestGCD[35528:4611901] 1

原因:由于main queue是串行queue,采用FIFO执行任务,也就是block操作加在了队列之后,dispathc_sync堵塞了主线程等待block语句完成后执行main thread,但block语句由于线程阻塞永不会执行,所以导致一直等待死锁。

3.6 dispatch_asyc
与sync不同,它是非同步的追加到指定的dispatch queue中。dispatch_async函数不做任何等待。

这里也有一个对于该函数理解的题目,如下,推断他的执行顺序

NSLog(@"1");

dispatch_async(dispatch_get_main_queue(), ^{

NSLog(@"2");

});

for (int i = 0; i < 10; i++) {

NSLog(@"3");

}

打印log日志为:

2016-06-28 09:39:16.048 TestGCD[35575:4619720] 1

2016-06-28 09:39:16.049 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.049 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.050 TestGCD[35575:4619720] 3

2016-06-28 09:39:16.059 TestGCD[35575:4619720] 2

也就是说NSLog(@”2”);永远会在for循环之后执行。原因:main queue为串行队列,遵循FIFO原则,同时为异步执行,异步block添加到队列中的“不等待”立刻执行for循环,在下一次runloop时才会执行block语句块的内容。

3.7 dispatch_semaphore_t
dispatch semaphore是持有计数的信号,该计数是多线程中的计数类型信号。所谓信号,类似于过北京西站地铁口安检时常用的手牌。可以通过时举起手牌,不可通过时放下手牌。而在dispatch semaphore中,使用计数来实现该功能。计数为0时等待,计数为1或者大于1时,减去1而不等待。

主要涉及三个函数 dipatch_semaphore_create()、dispatch_semphore_signal、dispatch_semaphore_wait.

对于以上三个函数通常都用停车位来解释,dipatch_semaphore_create()说明了初始车位数,没调用一次dispatch_semphore_signal剩余车位数就增加一个,每调用dispatch_semaphore_wait剩余车位数减少一个,等车位数为0时,再来车(即调用dispatch_semaphore_wait)就只能等车位。

该函数同样能解决上节中A,B,C的执行依赖问题。

dispatch_semaphore_t semaphore1 = dispatch_semaphore_create(0);

dispatch_semaphore_t semaphore2 = dispatch_semaphore_create(0);

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@"A");

dispatch_semaphore_signal(semaphore1);

});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

NSLog(@"B");

dispatch_semaphore_signal(semaphore2);

});

dispatch_semaphore_wait(semaphore1, DISPATCH_TIME_FOREVER);

dispatch_semaphore_wait(semaphore2, DISPATCH_TIME_FOREVER);

NSLog(@"C");

// 输出

2016-06-28 10:01:33.658 TestGCD[35622:4632780] B

2016-06-28 10:01:33.658 TestGCD[35622:4632775] A

2016-06-28 10:01:33.660 TestGCD[35622:4632744] C

3.8 dispatch_apply
dispatch_apply函数是dispatch_sync函数和dispatch group的关联API。该函数按指定的次数将指定的block追加到指定的dispatch queue中,并等待全部处理执行完成。

dispatch_apply(5, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {

NSLog(@"%zu", index);

});

NSLog(@"done");

// 输出

2016-06-28 10:10:25.110 TestGCD[35649:4638944] 3

2016-06-28 10:10:25.110 TestGCD[35649:4638889] 0

2016-06-28 10:10:25.110 TestGCD[35649:4638940] 2

2016-06-28 10:10:25.110 TestGCD[35649:4638934] 1

2016-06-28 10:10:25.111 TestGCD[35649:4638889] 4

2016-06-28 10:10:25.111 TestGCD[35649:4638889] done

第一个参数为重复的次数,第二个参数为追加对象的dispatch queue,第三个参数为追加的处理。由于dispatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束,推荐在dispatch_async函数中非同步地执行dispatch_apply函数

文/XCodeMan(简书作者)
原文链接
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

参考
《Objective-C高级编程 iOS与OS X多线程》