转 Grand Central Dispatch 基础教程:Part 1/2 -swift

前端之家收集整理的这篇文章主要介绍了转 Grand Central Dispatch 基础教程:Part 1/2 -swift前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

本文转载,原文地址:http://www.cocoachina.com/ios/20150609/12072.html


原文Grand Central Dispatch Tutorail for Swift: Part 1/2

原文作者:Bjrn Olav Ruud

译者:Ethan Joe

尽管Grand Central Dispatch(以下简称为GCD)已推出一段时间了,但并不是所有人都明白其原理;当然这是可以理解的,毕竟程序的并发机制很繁琐,而且基于C的GCD的API对于Swift的新世界并不是特别友好。

在接下来的两节教程中,你将学习GCD的输入 (in)与输出 (out)。第一节将解释什么是GCD并了解几个GCD的基础函数。在第二节,你将学习几个更加进阶的GCD函数

Getting Started

GCD是libdispatch的代名词,libdispatch代表着运行iOS与OS X的多核设备上执行并行代码的官方代码库。它经常有以下几个特点:

  • GCD通过将高代价任务推迟执行并调至后台运行的方式来提升App的交互速度。

  • GCD提供比锁与多线程更简单的并发模型,以此来避免一些由并发引起的Bug。

  • @H_404_55@

    为了理解GCD,你需要明白一些与线程、并发的相关的概念。这些概念间有着细微且模糊的差别,所以在学习GCD前请简略地熟悉一下这些概念。

    连续性 VS 并发性

    这些术语用来描述一些被执行的任务彼此间的关系。连续性执行任务代表着同一时间内只执行一个任务,而并发性执行任务则代表着同一时间内可能会执行多个任务。

    任务

    在这篇教程中你可以把每个任务看成是一个闭包。 事实上,你也可以通过函数指针来使用GCD,但在大多数情况下这明显有些麻烦。所以,闭包用起来更简单。

    不知道什么是Swift中的闭包?闭包是可被储存并传值的可调用代码块,当它被调用时可以像函数那样包含参数并返回值。

    Swift中的闭包和Objective-C的块很相近,它们彼此间是可以相互交替的。这个过程中有一点你不能做的是:用Objective-C的块代码去交互具有Swift独有属性属性的闭包,比如说具有元组属性的闭包。但是从Swift端交互Objective-C端的代码则是毫无障碍的,所以无论何时你在文档中看到到的Objective-C的块代码都是可用Swift的闭包代替的。

    同步 VS 异步

    这些术语用来描述当一个函数的控制权返回给调用者时已完成的工作的数量

    同步函数只有在其命令的任务完成时才会返回值。

    异步函数则不会等待其命令的任务完成,即会立即返回值。所以,异步函数不会锁住当前线程使其不能向队列中的下一位函数执行。

    值得注意的是---当你看到一个同步函数锁住(block)了当前进程,或者一个函数是锁函数(blocking function)或是锁运算(block operation)时别认混了。这里的锁(blocks)是用来形容其对于自己线程的影响,它跟Objective-C中的块(block)是不一样的。再有一点要记住的就是在任何GCD文档中涉及到Objective-C的块代码都是可以用Swift的闭包来替换的。

    临界区

    这是一段不能被在两个线程中同时执行的代码。这是因为这段代码负责管理像变量这种若被并发进程使用便会更改的可共享资源。

    资源竞争

    这是一种软件系统在一种不被控制的模式下依靠于特定队列或者基于事件执行时间进行运行的情况,比如说程序当前多个任务执行的具体顺序。资源竞争可以产生一些不会在代码排错中立即找到的错误

    死锁

    两个或两个以上的进程因等待彼此完成任务或因执行其他任务而停止当前进程运行的情况被称作为死锁。举个例子,进程A因等待进程B完成任务而停止运行,但进程B也在等待进程A完成任务而停止运行的僵持状态就是死锁。

    线程安全性

    具有线程安全性的代码可以在不产生任何问题(比如数据篡改、崩溃等)的情况下在多线程间或是并发任务间被安全的调用。不具有线程安全性的代码的正常运行只有在单一的环境下才可被保证。举个具有线性安全性的代码示例let a = ["thread-safe"]。你可以在多线程间,不产生任何bug的情况下调用这个具有只读性的数组。相反,通过var a = ["thread-unsafe"]声明的数组是可变可修改的。这就意味着这个数组在多线层间可被修改从而产生一些不可预测的问题,对于那些可变的变量与数据结构最好不要同时在多个线程间使用。

    上下文切换

    上下文切换是当你在一个进程中的多个不同线程间进行切换时的一种进程进行储存与恢复的状态。这种进程在写多任务App时相当常见,但这通常会产生额外的系统开销。

    并发 VS 并行

    并发和并行总是被同时提及,所以有必要解释一下两者间的区别。

    并发代码中各个单独部分可以被"同时"执行。不管怎样,这都由系统决定以何种方式执行。具有多核处理器的设备通过并行的方式在同一时间内实现多线程间的工作;但是单核处理器设备只能在同一时间内运行在单一线程上,并利用上下文切换的方式切换至其他线程以达到跟并行相同的工作效果。如下图所示,单核处理器设备运行速度快到形成了一种并行的假象。

    并发 VS 并行

    尽管你会在GCD下写出使用多线程的代码,但这仍由GCD来决定是否会使用并发机制。并行机制包含着并发机制,但并发机制却不一定能保证并行机制的运行。

    队列

    GCD通过队列分配的方式来处理待执行的任务。这些队列管理着你提供给GCD待处理的任务并以FIFO的顺序进行处理。这就得以保证第一个加进队列的任务会被首个处理,第二个加进队列的任务则被其次处理,其后则以此类推。

    连续队列

    连续队列中的任务每次执行只一个,一个任务只有在其前面的任务执行完毕后才可开始运行。如下图所示,你不会知道前一个任务结束到下一个任务开始时的时间间隔。

    连续队列

    每一个任务的执行时间都是由GCD控制的;唯一一件你可以确保的事便是GCD会在同一时间内按照任务加进队列的顺序执行一个任务。

    因为在连续队列中不允许多个任务同时运行,这就减少了同时访问临界区的风险;这种机制在多任务的资源竞争的过程中保护了临界区。假如分配任务至分发队列是访问临界区的唯一方式,那这就保证了的临界区的安全。

    并发队列

    并发队列中的任务依旧以FIFO顺序开始执行。。。但你能知道的也就这么多了!任务间可以以任何顺序结束,你不会知道下一个任务开始的时间也不会知道一段时间内正在运行任务的数量。因为,这一切都是由GCD控制的。

    如下图所示,在GCD控制下的四个并发任务:

    并发队列

    需要注意的是,在任务0开始执行后花了一段时间后任务1才开始执行,但任务1、2、3便一个接一个地快速运行起来。再有,即便任务3在任务2开始执行后才开始执行,但任务3却更早地结束执行。

    任务的开始执行的时间完全由GCD决定。假如一个任务与另一个任务的执行时间相互重叠,便由GCD决定(在多核非繁忙可用的情况下)是否利用不同的处理器运行或是利用上下文切换的方式运行不同的任务。

    为了用起来有趣一些,GCD提供了至少五种特别的队列来对应不同情况。

    队列种类

    首先,系统提供了一个名为主队列(main queue)的特殊连续队列。像其他连续队列一样,这个队列在同一间内只能执行一个任务。不管怎样,这保证了所有任务都将被这个唯一被允许刷新UI的线程所执行。它也是唯一一个用作向UIView对象发送信息或推送监听(Notification)。

    GCD也提供了其他几个并发队列。这几个队列都与自己的QoS (Quality of Service)类所关联。Qos代表着待处理任务的执行意图,GCD会根据待处理任务的执行意图来决定最优化的执行优先权。

    • QOS_CLASS_USER_INTERACTIVE: user interactive类代表着为了提供良好的用户体验而需要被立即执行的任务。它经常用来刷新UI、处理一些要求低延迟的加载工作。在App运行的期间,这个类中的工作完成总量应该很小。

    • QOS_CLASS_USER_INITIATED:user initiated类代表着从UI端初始化并可异步运行的任务。它在用户等待及时反馈时和涉及继续运行用户交互的任务时被使用。

    • QOS_CLASS_UTILITY:utility类代表着长时间运行的任务,尤其是那种用户可见的进度条。它经常用来处理计算、I/O、网络通信、持续数据反馈及相似的任务。这个类被设计得具有高效率处理能力。

    • QOS_CLASS_BACKBROUND:background类代表着那些用户并不需要立即知晓的任务。它经常用来完成预处理、维护及一些不需要用户交互的、对完成时间并无太高要求的任务。

    • @H_404_55@

      要知道苹果的API也会使用这些全局分配队列,所以你分派的任务不会是队列中的唯一一个。

      最后,你也可以自己写一个连续队列或是并发队列。算起来你起码最少会有五个队列:主队列、四个全局队列再加上你自己的队列。

      以上便是分配队列的全体成员。

      GCD的关键在于选择正确的分发函数以此把你的任务分发至队列。理解这些东西的最好办法就是完善下面的Sample Project。

      Sample Project

      既然这篇教程的目的在于通过使用GCD在不同的线程间安全地调用代码,那么接下来的任务便是完成这个名为GooglyPuff的半成品。

      GooglyPuff是一款通过CoreImage脸部识别API在照片中人脸的双眼的位置上贴上咕噜式的大眼睛且线程不安全的App。你既可以从Photo Library中选择照片,也可以通过网络从事先设置好的地址下载照片。

      GooglyPuff Swift Start 1

      将工程下载至本地后用Xcode打开并编译运行。它看起来是这样的:

      GooglyPuff

      在工程中共有四个类文件

      • PhotoCollectionViewController:这是App运行后显示的首个界面。它将显示所有被选照片的缩略图

      • PhotoDetailViewController:它将处理将咕噜眼添加至照片的工作并将处理完毕的照片显示在UIScrollView中。

      • Photo:一个包含着照片基本属性的协议,其中有image(未处理照片)、thumbnail(裁减后的照片)及status(照片可否使用状态);两个用来实现协议的类,DownloadPhoto将从一个NSURL实例中实例化照片,而AssetPhoto则从一个ALAsset实例中实例化照片。

      • PhotoManager:这个类将管理所有Photo类型对象。

      • @H_404_55@

        使用dispatch_async处理后台任务

        回到刚才运行的App后,通过自己的Photo Library添加照片或是使用Le internet下载一些照片。

        需要注意的是当你点击PhotoCollectionViewController中的一个UICollectionViewCell后,界面切换至一个新的PhotoDetailViewController所用的时间;对于那些处理速度较慢的设备来说,处理一张较大的照片会产生一个非常明显的延迟。

        这种情况下很容易使UIViewController的viewDidLoad因处理过于混杂的工作而负载;这么做的结果便在view controller出现前产生较长的延迟。假如可能的话,我们最好将某些工作放置后台处理。

        这听起来dispatch_async该上场了。

        打开PhotoDetailViewController后将viewDidLoad函数替换成下述代码

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        overridefuncviewDidLoad(){
        super.viewDidLoad()
        assert(image!=nil,"Imagenotset;requiredtouseviewcontroller")
        photoImageView.image=image
        //Resizeifneccessarytoensureit'snotpixelated
        ifimage.size.height<=photoImageView.bounds.size.height&&
        image.size.width<=photoImageView.bounds.size.width{
        photoImageView.contentMode=.Center
        }
        dispatch_async(dispatch_get_global_queue(Int(QOS_CLASS_USER_INITIATED.value),0)){//1
        letoverlayImage=self.faceOverlayImageFromImage(self.image)
        dispatch_async(dispatch_get_main_queue()){//2
        self.fadeInNewImage(overlayImage)//3
        }
        }
        }

        在这里解释一下上面修改代码

        1. 你首先将照片处理工作从主线程(main thread)移至一个全局队列(global queue)。因为这是一个异步派发(dispatch_async的调用,闭包以异步的形式进行传输意味着调用的线程将会被继续执行。这样一来便会使viewDidLoad更早的在主线程上结束执行并使得整个加载过程更加流畅。与此同时,脸部识别的过程已经开始并在一段时间后结束。

        2. 这时脸部识别的过程已经结束并生成了一张新照片。当你想用这张新照片来刷新你的UIImageView时,你可以向主线程添加一个新的闭包。需要注意的是--主线程只能用来访问UIKit。

        3. 最后,你便用这张有着咕噜眼的fadeInNewImage照片来刷新UI。

        有没有注意到你已经用了Swift的尾随闭包语法(trailing closure Syntax),就是以在包含着特定分配队列参数的括号后书写表达式的形式了向dispatch_async传递闭包。假如把闭包写出函数括号的话,语法会看起来更加简洁。

        运行并编译App;选一张照片后你会发现view controller加载得很快,咕噜眼会在很短的延迟后出现。现在的运行效果看起来比之前的好多了。当你尝试加载一张大得离谱的照片时,App并不会在view controller加载时而延迟,这种机制便会使App表现得更加良好。

        综上所述,dispatch_async将任务以闭包的形式添加至队列后立即返回。这个任务在之后的某个时间段由GCD所执行。当你要在不影响当前线程工作的前提下将基于网络或高密度cpu处理的任务移至后台处理时,dispatch_asnyc便派上用场了。

        接下来是一个关于在使用dispatch_asnyc的前提下,如何使用以及何时使用不同类型队列的简洁指南: