Code ReviewiOS点点滴滴iOS开发技巧

GCD学习笔记

2015-12-22  本文已影响401人  要上班的斌哥

本文来源是基于http://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1 。这篇文章是基于该博客写的一个读书笔记,是对原文章的知识点进行重新梳理加工,加入我的个人理解,以及在工作中使用GCD的一些心得。本人能力有限,若是发现不足请在评论当中指出,我会在文章中更新,不胜感激!

GCD 特点?

1、GCD 通过将耗时任务放到后台线程执行来提交程序的响应性

2、相对于直接使用线程和锁的并发模型,GCD 更易于使用,且能够避免一些 bug

3、代码执行效率高

4、所有相关的方法都是以dispatch命名开头

并发的相关概念

1、串行和并行

串行是同一个时间内有且仅有一个任务在执行(场景类似单人过一线天,人可以比作任务),并行是同一个时间内可以有多个任务在执行(场景类似多人百米赛跑,人可以比作任务)。在GCD中,任务可以理解成一个Block。

2、同步和异步

同步是提交任务之后会等待提交的任务执行完成之后才做返回操作(场景类似监考,监考老师从考试开始时就一直呆在考室里面,直到考试结束后才走出考室)。异步就是提交任务之后,不管任务是否开始执行,立即做返回操作(场景类似布置作业,老师从上课开始来布置一次作业,然后让各位同学完成作业,布置完作业之后,老师马上就离开了)。

3、临界值

临界值可以理解成一块不能并发执行的代码,这块代码包含了一些共享的资源,不能允许多个线程同时访问(在几个口渴的人面前,只有一个水壶,这几个人只能一个一个直接用水壶喝水,水壶就是临界值)。

4、竞争条件

简而言之,多个线程在读写共享资源时,执行结果取决于线程的执行时间和执行顺序(在几个口渴的人面前,只有一个水壶,这几个人只能一个一个直接用水壶喝水,这几个人当中一个人能喝多少水取决于这个人是什么时候开始喝的水以及喝了多长时间)。

5、死锁

死锁是线程中比较悲剧的一个问题,简单来说就是,线程A等着线程B执行完成,线程B反过来等着线程A执行完成,好了,这样线程AB就一直耗着了,这就是死锁(用一个场景来描述这个死锁,男生心里暗恋一个女生,碰巧该女生也暗恋这个男生,2个人都在等着对方捅破最后的窗户纸,最后2人就没有浪漫的结果了,哈哈)。

6、线程安全

线程安全是一个比较常见的概念,线程安全的代码能够在多线程和并发访问下不会出现数据错误等问题,比如NSDictionary就是线程安全,它可以在同一时间内被多个线程访问而不会有任何问题,对比之下NSMutableDictionary就是非线程安全的,在同一个时间内只能有且只有一个线程操作它。(GCD的队列就是线程安全的)

7、上下文切换

上下文切换就是在一个进程内执行多线程任务时候,线程之间切换时候保存和恢复线程的执行状态,比如从主线程切换到其它线程,这个时候系统会保存主线程的执行状态(包括不限于变量),从其它线程切换到主线程时候系统会恢复之前保存的主线程执行状态。

讲完了并发的相关概念,接下来就开始讲和GCD直接相关的技术了(在本文中任务与Block是等同的)

队列

GCD提供了队列来处理任务(Block),队列使用FIFO(先进先出)的方法来管理你提交给GCD任务,也就是说在GCD的同一个队列里面,先提交的任务比慢提交的任务较先执行,也就是先来先执行。

1、串行队列(Serial Queues)

顾名思义,串行队列就是一个时间内只执行一个任务,只有前一个任务执行完成之后,才会开始执行下一个任务,任务的被执行顺序和任务被提交到GCD的顺序是一致的。示意图如下(来自网络)

行队列

2、有串行队列就会有并发队列(Concuttent Queues)

并发队列允许同一个时间内可以有多个任务在执行,不过和串行队列一致的一点就是任务的被执行顺序和任务被提交到GCD的顺序是一致的。对于GCD来说,任务先来先执行,不管你是什么类型的队列。示意图如下(来自网络)

并发队列

既然知道了并发队列和串行队列,那你也应该会猜到苹果肯定也会给几个默认的队列让我们这些开发者使用的,接下来就来说说队列的类型。系统提供了一个特殊的串行队列给我们使用,这个队列就是主线程队列(main queue),它除了具备串行队列的一个特点之外(一个时间内只能执行一个任务,只有前一个任务执行完成之后,才会开始执行下一个任务)还有一个大杀器,就是所有的任务保证都会在主线程中执行,这意味着什么呢?意味着我们可以使用这个队列来更新UI。(不管iOS还是Android系统,所有的UI操作都是要在主线程中来完成的)。

说完串行队列说说系统提供的并发队列吧!名字叫做全局分发队列(Global Dispatch Queue),它有四个不同优先级别的并发队列,优先级别分别是Background,low,default,high。使用这个全局分发队列的时候并不是设置优先级别越高越好,因为苹果的Api也是使用这些队列,所以在这些个队列中已经有存在一些任务,通常的使用方法是设置默认的优先级别default。

关键点 dispatch_async,dispath_sync,串行队列,并行队列

讲了这么多,还没有上代码,好烦。我就图方便,直接用他人的现有案例来说吧,http://cdn4.raywenderlich.com/wp-content/uploads/2014/01/GooglyPuff_Start_1.zip  (初始案例)

这是一个图片案例,从网络上加载图片,加载完成后点击进入查看大图,在大图里面识别出人物的眼睛,并给眼睛贴上图片。

项目截图 案例结构说明


在开发中,为了有较好的用户体验,通常会把一些耗时的操作(文件操作,数据库操作,网络操作,图片编解码)放到后台线程去操作,以避免耗时操作阻塞主线程。在被注释掉的代码中直接在UI主线程进行图片对象的处理(对人物的眼睛进行识别然后贴图),这个是一个耗时操作,所以对它进行改进。

改进之后,

1、将这个耗时操作放到后台线程去操作,使用 dispath_async 异步提交任务到全局并发队列(优先级为高),然后不管任务现在是否开始执行,立即执行下一个代码NSLog打印内容。

2、在第二个dispath_async中,此时上下文环境为后台线程,那么要更新UI的话必须要在主线程内更新,所以使用dispath_async异步提交更新UI的任务到主线程(dispatch_get_main_queue()获取串行主线程队列),dispath_async为异步执行,不会等待任务执行完返回,而是立即返回。

3、使用UIImage对象更新UI。

改进之后

从上面的代码可以看到,dispath_async添加Block到一个队列中然后马上返回,这个Block在之后的某个时间点会被GCD执行。那么什么时候使用dispath_async呢?网络操作和CPU密集型的任务都可以放到后台去执行,这样做的好处就是不会阻塞当前主线程。

dispath_async和各个类型的队列配合:

1、自定义串行队列

如果你想要在后台线程中连续执行任务,并且跟踪任务的执行,那么dispath_async+自定义串行队列是你的一个不错的选择。

自定义串行队列

2、主线程串行队列

在后台并发队列执行完任务之后,通常会跳到主线程更新UI。

主线程串行队列

3、后台线程执行耗时的非UI任务

dispath_sync和各个类型的队列配合:

1、自定义串行队列,在主线程内同步执行dispath_sync,将任务提交到自定义串行队列执行。

2、主线程串行队列,在主线程同步提交任务到主线程串行队列会造成死锁,死锁,死锁。

代码中的dispath_sync提交任务到主线程串行队列之后,没有马上返回而是在等待任务执行完成。而在主线程串行队列,已经有dispath_sync这个方法在执行了,主线程队列等dispath_sync这个方法执行完成后,才执行dispath_sync的提交任务。dispath_sync提交的任务等待主线程,主线程等待dispath_sync方法,你等我,我等你,大家一起Over!

3、并发队列,dispath_sync和并发队列一起使用是无法达到任务在后台运行的效果的,因为并发队列的任务都是在主线程中,并且无法体现出并发队列的并发特性

关键点 dispatch_after

有些时候我们会有类似需求,比如在用户登录成功几秒后开始从服务器同步数据到本地。可以使用dispatch_after来完成类似延迟任务。在当前案例中,我们使用dispatch_after来延迟做出提示。

先创建一个dispatch_time_t 说明从什么时候开始算起延迟多少时间,延迟时间的单位是毫秒。在延迟时间到达之后dispatch_after提交任务给GCD。GCD根据情况安排任务的执行。也就是说这里的延迟是延迟任务的提交时间,而不是说延迟多少时间后任务开始执行。

dispatch_after和各个类型的队列

1、自定义串行队列(Custom Serial Queue),不建议在这个队列中使用。

2、主线程队列(Main Queue)使用dispatch_after的好地方。

3、并发队列(Concurrent Queue,不建议在这个队列中使用

关键点 线程安全的单例对象

在使用单例模式的时候,最重要的就是当有多个线程同时访问单例对象的时候,单例对象需要时刻保持有且只有一个对象,数据读写线程安全。先看看案例

这个sharedManager方法如果有多个线程同时访问的话,那么就会出现创建多个sharedPhotoManager对象的问题。启用2个并发任务来访问sharedManager方法,

sharedManager方法做一些休眠操作和打印内存地址操作,这样的目的就是让2个线程同时可以访问sharedManager方法

Xcode输入的sharedPhotoManager对象内存地址表明,创建了3个sharedPhotoManager对象,那么这个作为单例肯定是很坑爹的。

关键点 dispatch_once

dispatch_once()方法会以线程安全的方式有且仅有一次执行Block里面的方法,这样当多个线程同时访问sharedManager方法的时候就不用担心会出现多个对象被创建的情况了,哈哈,简单吧!

说完了初始化的问题,接下来就是getter和setter方法的问题了。dispatch barriers是GCD提供的优雅的读写锁解决方案。

看图说话,在dispatch barriers 执行前任务是并发执行的,但是到了dispatch barriers后,并发执行全部都变成了串行执行,当dispatch barriers执行完毕后,任务恢复成以前的状态也就是并发执行。也就是说在当前的队列中,当任务执行到dispatch barriers的Block的时候,dispatch barriers可以保证此时有且仅有这个Block在执行,当这个Block执行成功后,队列恢复正常。(这个类似多人过独木桥,有多个人并排走到一座独木桥前,这个独木桥每次只能一个人通过,且桥上一次只能站一个人,那么多个人就是单个单个的过独立桥,过桥之后恢复过桥之前的状态)。

dispatch barriers和各个类型的队列

1、自定义串行队列(Custom Serial Queue), 串行队列本来就是一次只能执行一个任务和dispatch barriers搭配没有什么用处。

2、并发队列(Global Concurrent Queue),系统的API也有使用这些队列,为了不影响系统API执行,最好不要在这些队列使用。

3、自定义并发队列(Custom Concurrent Queue),墙裂推荐,需要保证setter和初始化方法线程安全的都是可以使用这个搭配。

解决getter和setter方法的线程安全问题

dispatch_barrier_async在setter方法保证往_photosArray加入photo是线程安全的,至于getter方法使用NSArray保证了array不会被误修改.

最后的工程 http://cdn2.raywenderlich.com/wp-content/uploads/2014/01/GooglyPuff_End_1.zip

如果你觉得我的这篇文章对你有一丁点儿作用的话,那么希望你能在下方给个赞哈,让我知道这文章已经起了它应该的作用,谢谢!

上一篇下一篇

猜你喜欢

热点阅读