iOS中的多线程技术

多线程

多线程技术是iOS开发里十分常见的,下面会介绍GCD的常用多线程技术。首先简单了解一下几个概念:

  • 同步,异步:任务执行时是否开多线程,同步为单线程,异步为多线程
  • 串行,并发:任务执行的方式,串行为顺序执行,并发为同时执行
  • 队列:任务存放的地方,线程从队列里取出任务执行。分为串行队列和并发队列
  • 线程:执行任务的实际运作单位,主线程从主队列里取任务,更新UI必须在主线程完成

GCD:

GCD是操作最简单,也是我们最常用的多线程技术。在OC里是一套C语言API,在Swift里对其封装进一步简化,本文会使用Swift语法。

一。主队列,全局队列

先来看看这个GCD的最常用公式:

DispatchQueue.global().async {   
    //long time operation
    DispatchQueue.main.async {   
        //update UI
    }
}

它分为以下几个步骤:

  • 通过DispatchQueue.global()获取全局队列
  • 在全局队列中async并发执行耗时操作,如下载
  • 通过DispatchQueue.main获取全局队列
  • 在全局队列中async并发执行更新UI的动作

那么为什么要如此使用呢?

  • 首先,更新UI的操作必须放在主线程中,否则会报错。这里DispatchQueue.main是获取主队列,它是一个串行队列,而主线程要执行的任务就是到这个队列里去取的。
  • DispatchQueue.global()是获取系统提供的全局队列,该队列是并发队列。加入该队列的任务会开辟多条线程共同执行,就好像一个页面会下载显示很多张图片,用全局队列的话图片会一张张显示出来,而如果用主队列的话,所有图片下载完成才统一显示。
  • 这里不管是主队列还是全局队列都使用async,而没有使用sync是因为sync同步操作,相当于将所有操作都由主线程来执行,这会阻塞主线程,也就是UI上的卡顿。

这里写了一个小实验比较上面的各种情况。(因为图片地址在公司服务器上,这里不公布代码,只看结果)。

点击一个button,会Push到下一个页面,而下一个页面中会有一个CollectionView,共包含10个CollectionViewCell,每一个cell中都会下载并显示一张图片。

  • 第一张图用的是之前的标准公式,点击按钮后立即Push到下一个页面,并且图片也相继显示出来。

    GCDDemo1.gif
  • 第二张图是将DispatchQueue.global()换成了DispatchQueue.main()。可以看到,点击按钮后立即Push到下一个页面,所有图片下载完成后再一起显示,这是因为主队列是串行队列,所有的更新UI任务都排在了下载任务之后,因此要等所有的图片下载完成后,UI才会开始显示。

    [图片上传中...(GCDDemo3.gif-83a055-1564041778608-0)]
  • 第三张图是将DispatchQueue.global().async换成了DispatchQueue.global().sync。可以看到,点击按钮后卡顿了一会儿,再进入下一个页面。这是因为sync是同步操作,会阻塞主线程。

    GCDDemo3.gif

由以上三张图可看出,常用公式是最合适的选择


二。创建队列

let queue = DispatchQueue(label: "com.demo.queue", qos: .background, attributes: .concurrent)
  • qos是指队列的优先级,从background到userinteractive,优先级从低到高
  • concurrent指定了该队列为并发队列,若不穿,则默认为串行队列

自建的队列最终会被归入到主队列和全局队列中去,那么为什么还要创建它们呢,也是便于对一系列任务的管理。


三。派发组DispatchGroup

开发中也许会有这一种情况,我们要同时请求好几个接口后,再刷新UI。这种对多个异步请求的管理,用DispatchGroup最合适。

用法如下

let group = DispatchGroup()

group.enter()
//task1
group.leave()

group.enter()
//task2
group.leave()

group.enter()
//task3
group.leave()

group.notify(queue: DispatchQueue.main) {
    //update UI
    print("更新UI")
}

更新UI的操作会在task1,task2,task3这3个异步任务完成后再执行。


四。信号量semaphore

简单的说,在异步操作中,任务完成的顺序是不确定的。semaphore可以使得我们将异步操作按顺序同步完成。

这里有一片博客,对其进行了详细的介绍


五。屏障barrier

想象有这样一个操作。从数据库里执行两次读的任务read1和read2,并发执行并没有什么问题。可如果要在read1和read2中间加入一个write1,要求read1读取的是write之前的数据,read2读取的是write之后的数据,那么应该如何处理呢?

首先,如果write不能用普通的并发操作。因为并发队列的特性是无法确保read1,read2以及write的执行顺序,这可能会发生read2读取的是write之前的数据。

这里我们需要用到barrier。顾名思义,它是作为一个屏障,隔开了read1和read2,在这个屏障内的进行write,也会保证,read1->write->read2这样的一个执行顺序

DispatchQueue.global().async {
    //read1
}
DispatchQueue.global().async(flags: .barrier) {
    //write
}
DispatchQueue.global().async {
    //read2
}

六。延迟执行

这里用GCD封装一个延迟执行

func delay(_ timeInterval: TimeInterval, closure: @escaping ()->()) {
    DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + timeInterval, execute: closure)
}

调用起来非常简单和明了

delay(3) {
        //task  
}

值的一体的是,该延迟执行指的并不是在3秒后执行task,而指的是,在3秒后,将task加入主队列。所以当主队列有大量处理或者本身已经延迟的情况下,真正执行的时间会比3秒更长。不过一般来说,在主队列非阻塞的情况下,该方法还是算非常简洁有效的。


七。创建单例

得益于GCD中的dispatch_once,这个函数会让程序只执行一次,我们在OC中用它来实现单例。

static dispatch_once_t pred;
dispatch_once(pred, ^{
    //init
});

不过为了防止单例类通过alloc或者new的方法实例化,实际上要写的代码还有很多,相比较下来Swift的单例实现就要简单很多。

class Singleton {
    static let shared = Singleton()
    private init() {}
}

八。死锁

死锁一般是指同步任务的相互等待,造成程序崩溃。

例如,我们在ViewController的ViewDidLoad里加入这一句,程序执行到这里立刻就会崩溃。

DispatchQueue.main.sync {}

因为ViewDidLoad是从主线程执行,而DispatchQueue.main.sync {}也是在主线程里同步执行。ViewDidLoad需要等待DispatchQueue.main.sync {}完成,但ViewDidLoad又是先加入主队列的,因此DispatchQueue.main.sync {}要执行必须先等ViewDidLoad执行完毕。因此他们相互等待,造成死锁。

你可能感兴趣的:(iOS中的多线程技术)