首页 >Swift

Swift 3使用GCD和DispatchQueues

2017-02-23 02:26 编辑: suiling 分类:Swift 来源:CocoaChina翻译小组

为了更清晰的表达,相关技术词汇不会被翻译,欢迎指正。     

中央处理单元(CPU)自存在以来最大的改进之一是包含了多核,并在多核上运行多线程,这意味着CPU在特定时刻可以同时运行多个任务。

只能串行执行任务或伪多线程多年前就已成为历史,串行? 伪多线程是什么鬼? 如果你听过这些, 说明你是个老腊肉并且有接触过旧式电脑,或者你亲自在旧式电脑上用过旧系统。但是不论

CPU有几核或它有多强, 如果你不使用它这些优点的话一切都是毫无意义,正是意识到这点, 多任务处理和多线程编程慢慢进入人们的视野, 开发者不仅可以, 而且应该在实际开发中尽可

的将程序分成能够在多个线程上并发执行的代码块来充分发挥CPU的多任务处理能力。

多核多线程编程好处有很多:更快的执行, 更好的用户体验, 界面更流程等等。 想象下你在`main thread`选择下载多张图片, 然后整个界面直接被卡死了, 60秒后终于下载完成, 然后你欣慰的看到了下载的图片,这样的应用还能让人愉悦的玩耍么?

在iOS中,苹果提供了两种方法来进行多任务处理:`Grand Central Dispatch`(以下简称`GCD`)和`NSOperationQueue`框架。 当你需要在`非main thread`或`非 main queue`上运行相关任务时, 它俩都能非常完满的完成任务。要用哪个,这个你自己来决定,本篇教程我们只探讨`GCD`的用法。在开始之前我们先了解一条任意时刻都必须遵守的规则:**主线程用于界面更新和用户交互,任何耗时或者耗CPU的任务必须在concurrent queue或者background queue上运行**。小白可能很难领会为什么要这么做,老鸟告诉你记住并遵守就好了。

`GCD`首次在iOS 4中引入,在实现并发,高性能和并发任务中表现出良好的灵活性和可配置性。但在`Swift3`之前它都跟天书一样,与`swift`格格不入的古董C语言风格,晦涩难记的方法名都让你望而却步,码农们宁愿用`NSOperaionQueue`都不用`GCD`, 稍微的搜索了解下你就会明白有多糟糕。

`Swift3`中`GCD`的用法发生了巨大变化,全新的`Swift`风格语法上手更简单,正是这些变更驱使我写下这篇教程来介绍`Swift3`下关于`GCD`的日常用法和注意事项,如果你用过旧语法的`GCD`(即使就使用了那么一点),那新语法你也一样可以驾轻就熟。

在正式开始前,首先来了解下`GCD`的一些基本知识:

1. `dispatch queue`: 最基本也最重要,其实就是一堆在主线程(或后台线程)上同步(或异步)来执行的代码,一旦被创建出来,操作系统就开始接手管理,在CPU上分配时间片来执行队列内的代码。开发者没法参与`queue`的管理。队列采用`FIFO模式`(先进先出),意味着先加入的也会被先完成,可以想象下超市排队买单, 队伍前的总是最先买单出去,后续会有示例来详细说明(译者说明:对于`serial queue`来说的确是这样)。

2. `work item`:一段代码块,可以在queue创建的时候添加,也可以单独创建方便之后复用,你可以把它当成将要在`queue`上运行的一个代码块。`work items`也遵循着`FIFO模式`,也可以同步(或异步)执行,如果选择同步的方式,运行中的程序直到代码块完成才会继续之后的工作,相对的,如果选择异步,运行中的程序在触发了代码块就立刻返回了。后续也会有示例来详细说明。

3. `serial`(串行)vs`concurrent`(并行):`serial`将会执行完一个任务才会开始下一个,`concurrent`触发完一个就立即进入下一个,而不管它是否已完成。

如果你要在`main queue`上运行代码,那你要提高警惕,不能让代码块的运行占用了界面响应更新的时间片。重申下规则:**任何时候你都应该在主线程上更新UI**。如果你尝试在其他线程更新,实际情况是你将不知道界面会不会更新或者什么时候被更新????。虽然如此, 在更新ui前,你还是可以将更新UI前的相关准备工作在后台先运行完毕,例如你可以在`background queue`上下载图片数据,完成后你回到主线程来显示图片。

大部分情况下,你不用自己创建`queue`,系统已经帮你创建好了一些常用的全局`queue`,你可以直接使用它们来处理你想要运行的任务,你可能会关心队列将会在哪个线程上运行, 你是否能够指定线程来执行代码神马的, 实际情况是除了创建、配置、使用`queue`其他你什么都做不了。 iOS管理着了一个线程池, 意味着除了主线程还有一堆其他线程存在着,系统将会选择其中的一个或多个(依赖于你创建了多少个queue以及你创建queue的方式),具体选择哪个开发者是不知道的,操作系统会根据当前并发的任务,处理器的负载情况来做具体决定,但你真的想自己去管理这些么?

测试环境

为了更好的展示GCD的使用,我们本打算建立多个很小但很针对的示范工程, playground本来是个非常完美的解决方案, 但是因为playground不支持从不同线程上调用方法,最后只好妥协使用了一个普通的项目工程,你可以在这里下载它[https://github.com/appcoda/GCDSamples/raw/master/Starter_Project.zip]它

除了以下两点,整个工程项目几乎为空:    

1. 在`ViewController.swift`文件中你将会发现一系列没有被实现的方法,每个方法将会给我们演示一个GCD的特性,你需要做的就是在viewDidLoad(_:)方法中对代码取消注释以便执行相关的方法。    

2. 在`Main.storyboard`, `ViewController`场景中你会发现添加了一个`UIImageView`,并且它已经通过`IBOutlet`和`ViewController`相关联,之后我们会在一个模拟真实使用场景的demo中使用到它。

正式进入话题:        


Dispatch Queues入门

在Swift3中,创建`dispatch queue`方式如下:

let queue = DispatchQueue(label: "com.appcoda.myqueue")

只需给`queue`提供一个唯一标签,获取唯一标签的一个很有效的做法是将你的dns地址反序,如(“com.appcoda.myqueue”),这也是苹果推荐的做法。但这不是硬性要求,你可以使用任何唯一的字符串作为标签。label 不是创建`queue`的唯一参数,在后续介绍相关内容时会逐一讲到其他参数。

一旦队列创建完成,我们就可以用它来运行代码,你可以使用`sync`来同步运行或使用`async`来异步运行。 为了更简洁的演示,我们会先使用block(闭包)来提供可执行代码,后续会使用`DispatchWorkItem`对象来代替block(注意:block也可当成`Queue`里的一个`DispatchWorkItem`, 译者补充: DispatchWorkItem 的初始方法:init(qos:, flags:, block:), block作为它其中的一个初始参数)。这里我们只是简单的在`queue`中同步的打印0~9 

为了在控制台清晰的区分结果,我们添加了一个红点,在后续添加更多队列或任务执行时,带颜色的点可以帮助我们更好的区分不同`queue`里的任务。

将上述代码片段放入你建立的初始项目的`ViewController.swift`文件的`simpleQueues()`方法中,确保这个方法在`viewDidAppear(_:)`中没有被注释,然后运行它,查看Xcode控制台,你将会看到一堆数字被打印出来,单个queue输出并不能帮助我们理解GCD是怎么工作,为了对比,我们在`simpleQueues()`方法的block后面添加另外一个打印100~109的代码块(仅仅为了展示区别)

作为对比,红点代码块在`background Queue`上运行,而蓝点代码块将会在`main queue`上运行,观察运行结果你会发现程序被卡住了,直到红点代码块执行执行完毕,`main queue`上的蓝点代码块才会被继续执行,之所以如此是因为`queue`中代码是同步执行的,控制台输出如下:

QQ截图20170221171122.png

如果使用`async`方法会发生神马呢?它会让`queue`中的代码异步执行吗? 如果是这样的话,程序应该不会像上面结果那样卡住而应该在`queue`内任务都执行完成前返回主线程上执行第二个`for`循环,使用`async`方法更新下`queue`的运行方式:

现在,来看下Xcode的控制台

相比于`sync`执行结果,`async`显示的更加有趣,你会发现`main queue`的代码(第二个`for`循环)和我们`queue`在并发执行,自定义`queue`在开始得到了更多运行时间,但是这仅仅是因为优先级(后续会讲)。 通过这个示例我们搞懂了几件事:        

1. 使用async主线程和后台线程可以并行执行任务        

2. 使用sync则只能串行执行任务, 当前线程被卡住直到串行任务完成才继续

尽管示例非常简单,但它完美的展示了`queue`中的代码块是怎么同步或异步执行的。 我们也会将在后续的示例中继续采用上例中色彩缤纷的打印日志(特定的颜色代码特定`queue`中代码执行结果,不同颜色代表不同的`queue`).

服务等级(QoS)和优先级  

在使用GCD和DispatchQueues的时候我们经常需要告知操作系统App里的哪些任务比其他任务更重要,哪些优先级更高。`main queue`通常被用来处理UI和响应操作, 它拥有很高的优先级。 实际使用场景中,通过向系统提供QoS信息,iOS会根据你的配置合理的处理优先级并提供所需资源(如CPU执行时间),虽然最终所有的任务都将完成,但优先级会让一部分早完成、一部分晚完成。

确定任务重要和优先级的属性被称为GCD服务等级(GCD QoS),实际上,QoS是个基于具体场景的枚举类型,在初始队列时,可以提供合适的QoS参数来得到相应的权限,如果没有指定QoS,那么初始方法会使用队列提供的默认的QoS值,QoS所有可用选型可以查看[这里],开始之前,建议你仔细阅读,以下会概括的介绍下可用的QoS场景(QoS Case),或者称为QoS等级(QoS classes),从前到后,优先级从高到低:

  • userInteractive

  • userInitiated

  • default

  • utility

  • background

  • unspecified

回到Xcode项目,找到`queuesWithQoS()`方法,定义并初始化以下两个全新的`dispatch queues`:

let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.userInitiated)
let queue2 = DispatchQueue(label: "com.appcoda.queue2",qos: DispatchQoS.userInitiated)

注意,两个队列都使用相同的QoS等级,所以执行时它们拥有相同权限。 像之前做的那样,第一个队列包括一个`for`循环来显示0~9(带红点),第二个队列将执行另外一个`for`循环来显示100~109(带蓝点)。

0021.png

查看下拥有相同权限(相同QoS)的队列执行结果        

记得对viewDidAppear(_:)中的queuesWithQos()方法取消注释

223.png

不难发现,两个队列的任务被"均匀"的执行了,这正是我们所期待的结果 现在,如下图所示将`queue2`的QoS等级改为更低的utility

let queue2 = DispatchQueue(label: "com.appcoda.queue2",qos: DispatchQoS.utility)

查看结果:

224.png

毫无悬念,优先级更高的`queue1`比`queue2`更快被执行,虽然在`queue1`运行的时候`queue2`得到一个运行的机会,系统还是将资源倾斜给了被标记为更重要的`queue1`,等`queue1`内的任务全部被执行完成,系统才开始全心全意服务于`queue2` 。

现在开始做另外一个实验: 这次将第一个队列QoS等级改为background:

let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.background)

让我们看下使用background这个次低的优先级会发生什么:

206.png

这次因为 utility QoSbackground QoS 优先级更高,第二个队列更快被完成。

通过上述例子,我们了解了QoS等级的使用方法,但是如果我们同时在主线程执行一个任务会发生什么? 在方法的后面添加以下代码:

207.png

同时赋予第一个队列更高的权限

let queue1 = DispatchQueue(label: "com.appcoda.queue1",qos: DispatchQoS.userInitiated)

运行结果如下:

208.png

实践证明: `main queue`默认就有一个很高的权限(译者注:实际运行发现也只是utility????),`queue1`和`main queue`并发执行,`queue2`在其他两个队列运行时没有得到多少运行的机会,最后才完成 。

并发队列(Concurrent Queues)

目前为止,我们已经看到dispatch queues是怎样同步和异步工作以及服务等级是怎么影响系统服务的优先等级,可能你会发现,上述所有示例中所有的`queue`都是串行执行的(serial),意味着如果你分配多个任务给任意一个队列,这些任务将会一个接一个被完成, 而不是同时完成。在这小节中,我们将会展示怎样让多任务(多工作项)同时运行,或者换种说法,怎么创建一个并发的队列。

回到项目,找到`concurrentQueues()`方法(记得取消对该方法的注释) ,在这个新方法里创建一个如下的新队列:

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility)

接下来,在队列中分配任务(或者称工作项):

209.png

结果如下所示,任务被顺序的执行:

210.png

接下来,修改`anotherQueue`队列初始化方法:

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility,attributes: .concurrent)

上述代码中添加了一个新参数`attributes`,当这个参数被赋予`concurrent`时,这个队列所有的任务将被同时执行。 如果你不加这个参数,那么队列默认就是串行执行的(serial)。 QoS不是必填项,在初始的时候完全可以忽略它。

再次运行代码,可以发现任务被高度并发执行了:

2110.png

注意到更改Qos等级同样可以影响到任务的执行,无论如何,只要你初始了一个并发队列,那么并发执行的任务会被同等对待,所有任务都将获得被执行的机会

`attrubites`参数可以选择另外一个值:`initiallyInactive`。 通过定义为`initiallyInactive` 队列任务不会自动开始执行,需要开发者主动去触发。 为了展示这个特性,需要建一个`inactiveQueue `队列:

var inactiveQueue: DispatchQueue!

初始化一个队列,然后赋值给`inactiveQueue `

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .utility,attributes: .initiallyInactive)
inactiveQueue = anotherQueue

为了让`concurrentQueues()`这个方法外的其他方法也可以 这个示例中的`inactiveQueue `属性非常必要,因为`anotherQueue`的作用域仅在方法中。 当方法执行完毕,它就对应用不可见了,根本不可能被方法外的其他方法来使用。(译者注:这里写的不太好,如果对象可能为空, 应该果断用optional,而不应该使用!再去判断是否空, inactiveQueue 应该被声明为optional类型:`var inactiveQueue: DispatchQueue?`)

再次运行应用,你将发现根本没有输出,这是我们所期待的结果,通过在`viewDidAppear(_:)`方法中添加一下代码来触发队列的运行:

if let queue = inactiveQueue {
    queue.activate()
}

DispatchQueue类的`activate()`让任务执行,因为队列没有标记为并发,所以它将顺序执行:

700.png

现在问题来了:,怎么创建一个既是初始不活跃又是并发的队列? 很简单,与之前提供`attributes`一个参数不同,这次给它赋值为一个包含两者的数组

let anotherQueue = DispatchQueue(label: "com.appcoda.anotherQueue",qos: .userInitiated,attributes: [.concurrent,.initiallyInactive])

结果如下:

320.png

延迟执行

有时你需要应用的某个流程中的某项任务延迟执行,GCD允许你执行某个方法来达到特定时间后运行你指定任务的目的。

这次我们要使用的是初始项目中的`queueWithDelay()`方法,首先添加以下代码:

let delayQueue = DispatchQueue(label: "com.appcoda.delayqueue",qos: .userInitiated)
print(Date())
 
let additionalTime: DispatchTimeInterval = .seconds(2)

一开始像平常那样创建一个`DispatchQueue`供后续代码使用;然后我们打印当前时间用于对比延迟任务对比。 延迟时间通常是个添加在`DispatchTime`值后面的`DispatchTimeInterval ` 枚举(值类型为整型) ,用于说明延迟详情(后续会讲到)。在示例中,任务设置为延迟两秒执行,这里使用的是`seconds`方法,除了这个,还提供了其他时间选项可供选择:

  • milliseconds (毫秒,千分之一秒)

  • microseconds (微秒,千分之一毫秒)

  • nanoseconds  (纳秒,千分之一微秒)

说明完毕,开始使用队列:

delayqueue.asyncAfter(deadline: .now() + additionalTime) {
    print(Date())
}

`now()`方法返回当前时间,在此基础上添加我们想要延迟的时间,运行代码,控制台将输出如下结果:

701.png

确实,正如预期那样,队列的任务2秒后才被执行。注意如果你不想使用一些预定义的方法来指定等待时间,你还有另外一种选择,直接在当前时间后直接添加一个`Double`值:

delayqueue.asyncAfter(deadline: .now() + 0.75) {
    print(Date())
}

在这种情况下,任务将在0.75秒后被执行。 你可以不适用`now()`方法,但是你必须自行提供一个DispatchTime值,以上展示的就是在队列中实现最简单的延迟任务,实际上这也是你所需要掌握的延迟实现方法的全部内容。

访问主队列和全局队列

前面的例子的`queue`都是通过手工创建得到,实际使用中并不总要你自己来做这个,特别是你不想改变队列的属性,在博文开始的地方我提到了系统会创建一系列的被称为全局队列的后台队列。 你可以像自己创建的队列一样使用它们,只要不要太滥用就行。

访问一个全局队列方法如下:

let globalQueue = Dispatchqueue.global()

你可以像我们之前使用自定义队列那样使用它:

702.png

使用全局变量,你只能使用部分属性,例如服务等级(Qos class)

let globalQueue = Dispatchqueue.global(qos: .userInitiated)

如果你不指定该参数(像第一个用例那样),那么Qos属相将采用默认的`default`值

抛开全局队列,你毋庸置疑会经常使用到主队列,最有可能是更新UI的时候。 接下来的代码块将展示在其他队列中运行主队列,根据你的需要,你可以指定同步或异步来执行你的代码:

Dispatchqueue.main.async {
    // Do something
}

在Xcode里,当你输入`Dispatchqueue.main.`(记住最后有个.),Xcode将会自动提示所有你能在主队列中访问的方法(实际上这个技巧适应于所有队列,操作方法是在队列名称后带`.`),上述代码就是主线程你所需要知道的内容,你也可以像自己创建队列那样,添加延迟执行代码块。

接下来将实操下主线程怎么更新UI,初始项目的`Main.storyboard`中有个`ViewController`场景包含了一个图片视图,通过`IBOutlet`属性连接到了`ViewController`类中,在`ViewController`类中找到`fetchImage()`方法, 补充上下载Appcoda logo和在图片视图中展示它的代码,如下所示(这里不讨论`URLSession`类和它的使用方法,自行脑补吧):

func fetchImage() {
    let imageURL: URL = URL(string: "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png")!
 
    (URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: imageURL,completionHandler: { (imageData,response,error) in
 
        if let data = imageData {
            print("Did download image data")
            self.imageView.image = UIImage(data: data)
        }
    }).resume()
}

你会发现代码中并没有在主线程中更新UI,而是在后台线程中运行`dataTask()`方法,编译运行下程序结果如下(记得调用`fetchImage()`方法):

703.png

虽然我们已经下载了图片并打印了相关信息,因为ui没有更新导致图片也没有显示出来,最有可能发生的情况是图片在打印信息的一段时间后显示(如果还有其他任务在运行,可能会要等更久),但是问题不止这一个,控制台输出了一大串关于在后台线程中更新ui的警告日志。

现在纠正到主线程来更新UI,将相关代码更新如下:

if let data = imageData {
    print("Did download image data")
 
    Dispatchqueue.main.async {
        self.imageView.image = UIImage(data: data)
    }
}

重新运行下app,因为主队列被唤起并更新了UI,图片在下载后立即就显示出来了。

使用DispatchWorkItem对象

DispatchWorkItem是一个代码块,它可以被分到任何的队列,包含的代码可以在后台或主线程中被执行,简单来说:它被用于替换我们前面写的代码block来调用

最简单的使用WorkItem的例子如下:

let workItem = DispatchWorkItem {
    // Do something
}

接下来展示一个小示例来阐述`DispatchWorkItem`的使用方法,找到`useWorkItem()`方法,在其中添加如下代码:

func useWorkItem() {
    var value = 10
 
    let workItem = DispatchWorkItem {
        value += 5
    }
}

workItem用于让value每次递增5,如下代码所示,通过调用`workItem`的`perform()`方法来激活

workItem.perform()

默认将会在主线程执行,但你可以选择在其他队列中执行,比如:

let queue = Dispatchqueue.global()
queue.async {
    workItem.perform()
}

这样也一样会运行正常。为了更简洁的实现同样目的,`DispatchQueue`提供了另外一个方法:

queue.async(execute: workItem)

当一个WorkItem被分发,你可以通知主队列(或其他队列)来做一些后续的处理:

workItem.notify(queue: Dispatchqueue.main) {
    print("value = ",value)
}

以上代码将会在控制台打印`value`对象的值,它将在workItem被分发后被调用,整理下代码,使用workItem的完整用法如下:

func useWorkItem() {
    var value = 10
 
    let workItem = DispatchWorkItem {
        value += 5
    }
 
    workItem.perform()
 
    let queue = Dispatchqueue.global(qos: .utility)
 
    queue.async(execute: workItem)
 
    workItem.notify(queue: Dispatchqueue.main) {
        print("value = ",value)
    }
}

以下是运行结果(`useWorkItem()`已经在`viewDidAppear(_:)`中被调用了:

2255.png

总结

这篇博文包含的内容应该满足大部分你将要使用到多任务和并发编程的场景,但也没有那么全面包含了GCD的方方面面,可能博文中有引到但是没有具体说明的内容,总体来说,我希望博文能够尽量的简单,从而被广大开发者所理解和接受。 如果你平常都没有用到GCD,那你应该考虑使用GCD来为你的`main queue`减负, 主动承担一些比较重的操作,能在后台运行的任务绝不在`main queue`上运行。 浅显易懂的GCD能够非常有效的加快应用的运行和响应速度, 你值得拥有。

本文demo下载地址见[GitHub]

译者杂耍时间:

Q1: 为啥代码中蓝点没有比红点先执行?

001.png

A1: 为了简洁说明, 去掉了for循环, 答案如下:

002.png

懵逼了, 好好的全局后端队列,怎么跑到主线程上去执行了. 答案就是苹果为了优化性能, sync会尽可能在当前线程来运行        

Q2: 探讨下崩溃问题:

003.png

好冤枉啊, 我不就想明明白白的在主线程上同步的运行么, 为啥说崩就崩了?

A2: 答案是死锁了

主线程是串行的, 上个任务执行完成才会继续下个任务, `simpleQueues()`整个方法相当于mainQueue的一个任务(任务A), 现在它里面加了个sync的{任务A1}, 意味着任务A1只有等任务A完成才能开始, 但是要完成任务A的话就必须先完成任务A1, 然后A又在等A1,然后就傻逼了, 逻辑好绕吧????....

搜索CocoaChina微信公众号:CocoaChina
微信扫一扫
订阅每日移动开发及APP推广热点资讯
公众号:
CocoaChina
我要投稿   收藏文章
上一篇:用RxSwift仿写知乎日报
下一篇:Swift里我用这个姿势写UserDefaults
我来说两句
发表评论
您还没有登录!请登录注册
所有评论(0

综合评论

相关帖子

sina weixin mail 回到顶部