iOS多线程——GCD与NSOperation总结

很长时间以来,我我的(可能还有不少同窗),对多线程编程都存在一些误解。一个很明显的表现是,不少人有这样的见解:编程

新开一个线程,能提升速度,避免阻塞主线程swift

毕竟多线程嘛,几个线程一块儿跑任务,速度快,还不阻塞主线程,简直完美。安全

 

在某些场合,咱们还见过另外一个“高深”的名词——“异步”。这东西好像和多线程挺相似,通过一番百度(阅读了不少质量层次不齐的文章)以后,不少人也没能真正搞懂何为“异步”。网络

 

因而,带着对“多线程”和“异步”的懵懂,不少人又开开心心踏上了多线程编程之旅,好比文章待会儿会提到的GCD。多线程

其实,若是不考虑其余任何因素和技术,多线程有百害而无一利,只能浪费时间,下降程序效率。并发

是的,我很清醒的写下这句话。app

试想一下,一个任务由十个子任务组成。如今有两种方式完成这个任务: 异步

1. 建十个线程,把每一个子任务放在对应的线程中执行。执行完一个线程中的任务就切换到另外一个线程。 
2. 把十个任务放在一个线程里,按顺序执行。async

操做系统的基础知识告诉咱们,线程,是执行程序最基本的单元,它有本身栈和寄存器。说得再具体一些,线程就是“一个CPU执行的一条无分叉的命令列”。ide

对于第一种方法,在十个线程之间来回切换,就意味着有十组栈和寄存器中的值须要不断地被备份、替换。 而对于对于第二种方法,只有一组寄存器和栈存在,显然效率完胜前者。

经过刚刚的分析咱们看到,多线程自己会带来效率上的损失。准确来讲,在处理并发任务时,多线程不只不能提升效率,反而还会下降程序效率。

 

所谓的“并发”,英文翻译是concurrent。要注意和“并行(parallelism)”的区别。

并发指的是一种现象,一种常常出现,无可避免的现象。它描述的是“多个任务同时发生,须要被处理”这一现象。它的侧重点在于“发生”。

好比有不少人排队等待检票,这一现象就能够理解为并发。

并行指的是一种技术,一个同时处理多个任务的技术。它描述了一种可以同时处理多个任务的能力,侧重点在于“运行”。

好比景点开放了多个检票窗口,同一时间内能服务多个游客。这种状况能够理解为并行。

并行的反义词就是串行,表示任务必须按顺序来,一个一个执行,前一个执行完了才能执行后一个。

咱们常常挂在嘴边的“多线程”,正是采用了并行技术,从而提升了执行效率。由于有多个线程,因此计算机的多个CPU能够同时工做,同时处理不一样线程内的指令。

并发是一种现象,面对这一现象,咱们首先建立多个线程,真正加快程序运行速度的,是并行技术。也就是让多个CPU同时工做。而多线程,是为了让多个CPU同时工做成为可能。

 

同步方法就是咱们平时调用的哪些方法。由于任何有编程经验的人都知道,好比在第一行调用foo()方法,那么程序运行到第二行的时候,foo方法确定是执行完了。

所谓的异步,就是容许在执行某一个任务时,函数马上返回,可是真正要执行的任务稍后完成。

好比咱们在点击保存按钮以后,要先把数据写到磁盘,而后更新UI。同步方法就是等到数据保存完再更新UI,而异步则是马上从保存数据的方法返回并向后执行代码,同时真正用来保存数据的指令将在稍后执行。

假设如今有三个任务须要处理。假设单个CPU处理它们分别须要三、一、1秒。

并行与串行,其实讨论的是处理这三个任务的速度问题。若是三个CPU并行处理,那么一共只须要3秒。相比于串行处理,节约了两秒。

而同步/异步,其实描述的是任务之间前后顺序问题。假设须要三秒的那个是保存数据的任务,而另外两个是UI相关的任务。那么经过异步执行第一个任务,咱们省去了三秒钟的卡顿时间。

对于同步执行的三个任务来讲,系统倾向于在同一个线程里执行它们。由于即便开了三个线程,也得等他们分别在各自的线程中完成。并不能减小总的处理时间,反而徒增了线程切换(这就是文章开头举的例子)

对于异步执行的三个任务来讲,系统倾向于在三个新的线程里执行他们。由于这样能够最大程度的利用CPU性能,提高程序运行效率。

因而咱们能够得出结论,在须要同时处理IO和UI的状况下,真正起做用的是异步,而不是多线程。能够不用多线程(由于处理UI很是快),但不能不用异步(不然的话至少要等IO结束)。

注意到我把“倾向于”这三个加粗了,也就是说异步方法并不必定永远在新线程里面执行,反之亦然。在接下来关于GCD的部分会对此作出解释。

iOS中多线程的实现方案  

GCD简介

GCD以block为基本单位,一个block中的代码能够为一个任务。下文中提到任务,能够理解为执行某个block

同时,GCD中有两大最重要的概念,分别是“队列”和“执行方式”。

使用block的过程,归纳来讲就是把block放进合适的队列,并选择合适的执行方式去执行block的过程。

  • 串行队列(先进入队列的任务先出队列,每次只执行一个任务) 
  • 并发队列(依然是“先入先出”,不过能够造成多个任务并发) 
  • 主队列(这是一个特殊的串行队列,并且队列中的任务必定会在主线程中执行)
  1. 同步执行 
  2. 异步执行

关于同步异步、串行并行和线程的关系,下面经过一个表格来总结

能够看到,同步方法不必定在本线程,异步方法方法也不必定新开线程(考虑主队列)。

然而事实上,在本文一开始就揭开了“多线程”的神秘面纱,因此咱们在编程时,更应该考虑的是:

同步 Or 异步以及串行 Or 并行,而非仅仅考虑是否新开线程。

固然,了解任务运行在那个线程中也是为了更加深刻的理解整个程序的运行状况,尤为是接下来要讨论的死锁问题。

 

在使用GCD的过程当中,若是向当前串行队列中同步派发一个任务,就会致使死锁。

这句话有点绕,咱们首先举个例子看看:

override func viewDidLoad() {  
    super.viewDidLoad()
    let mainQueue = dispatch_get_main_queue()
    let block = { ()  in
        print(NSThread.currentThread())
    }    
    dispatch_sync(mainQueue, block)
}

 这段代码就会致使死锁,由于咱们目前在主队列中,又将要同步地添加一个block到主队列(串行)中。

咱们知道dispatch_sync表示同步的执行任务,也就是说执行dispatch_sync后,当前队列会阻塞。而dispatch_sync中的block若是要在当前队列中执行,就得等待当前队列程执行完成。

在上面这个例子中,主队列在执行dispatch_sync,随后队列中新增一个任务block。由于主队列是同步队列,因此block要等dispatch_sync执行完才能执行,可是dispatch_sync是同步派发,要等block执行完才算是结束。在主队列中的两个任务互相等待,致使了死锁。

其实在一般状况下咱们没必要要用dispatch_sync,由于dispatch_async可以更好的利用CPU,提高程序运行速度。

只有当咱们须要保证队列中的任务必须顺序执行时,才考虑使用dispatch_sync。在使用dispatch_sync的时候应该分析当前处于哪一个队列,以及任务会提交到哪一个队列。

了解完队列以后,很天然的会有一个想法:咱们怎么知道全部任务都已经执行完了呢?

在单个串行队列中,这个不是问题,由于只要把回调block添加到队列末尾便可。

可是对于并行队列,以及多个串行、并行队列混合的状况,就须要使用 dispatch_group了。

let group = dispatch_group_create()

dispatch_group_async(group, serialQueue, { () -> Void in  
    for _ in 0..<2 {
        print("group-serial \(NSThread.currentThread())")
    }
})

dispatch_group_async(group, serialQueue, { () -> Void in  
    for _ in 0..<3 {
        NSLog("group-02 - %@", NSThread.currentThread())
    }
})

dispatch_group_notify(group, serialQueue, { () -> Void in  
    print("完成 - \(NSThread.currentThread())")
})

首先咱们要经过 dispatch_group_create() 方法生成一个组。

接下来,咱们把 dispatch_async 方法换成 dispatch_group_async。这个方法多了一个参数,第一个参数填刚刚建立的分组。

想问 dispatch_sync 对应的分组方法是什么的童鞋面壁思过三秒钟,思考一下 group 出现的目的和 dispatch_sync 的特色。

最后调用 dispatch_group_notify 方法。这个方法表示把第三个参数 block 传入第二个参数队列中去。并且能够保证第三个参数 block 执行时,group 中的全部任务已经所有完成。

dispatch_group_wait 方法是一个颇有用的方法,它的完整定义以下:

dispatch_group_wait(group: dispatch_group_t, _ timeout: dispatch_time_t) -> Int

第一个参数表示要等待的 group,第二个则表示等待时间。返回值表示通过指定的等待时间,属于这个 group 的任务是否已经所有执行完,若是是则返回 0,不然返回非 0。

第二个 dispatch_time_t 类型的参数还有两个特殊值:DISPATCH_TIME_NOW 和 DISPATCH_TIME_FOREVER

前者表示马上检查属于这个 group 的任务是否已经完成,后者则表示一直等到属于这个 group 的任务所有完成。

经过 GCD 还能够进行简单的定时操做,好比在 1 秒后执行某个 block 。代码以下:

let mainQueue = dispatch_get_main_queue()  
let time = dispatch_time(DISPATCH_TIME_NOW, Int64(3) * Int64(NSEC_PER_SEC))  
NSLog("%@",NSThread.currentThread())  
dispatch_after(time, mainQueue, {() in NSLog("%@",NSThread.currentThread())})  

 

dispatch_after 方法有三个参数。第一个表示时间,也就是从如今起日后三秒钟。第2、三个参数分别表示要提交的任务和提交到哪一个队列。

须要注意的是和dispatch_after仅表示在指定时间后提交任务,而非执行任务。若是任务提交到主队列,它将在main runloop中执行,对于每隔1/60秒执行一次的RunLoop,任务最多有可能在3+1/60秒后执行。

NSOperation 和 NSOperationQueue 主要涉及这几个方面:

  1. NSOperation 和 NSOperationQueue 用法介绍 
  2. NSOperation 的暂停、恢复和取消 
  3. 经过 KVO 对 NSOperation 的状态进行检测 
  4. 多个 NSOperation 的之间的依赖关系

从简单意义上来讲,NSOperation 是对 GCD 中的 block 进行的封装,它也表示一个要被执行的任务。

与 GCD 中的 block 相似,NSOperation 对象有一个 start() 方法表示开始执行这个任务。

不只如此,NSOperation 表示的任务还能够被取消。它还有三种状态 isExecutedisFinished和 isCancelled 以方便咱们经过 KVC 对它的状态进行监听。

想要开始执行一个任务能够这么写:

let operation = NSBlockOperation { () -> Void in  
    print(NSThread.currentThread())
}
operation.addExecutionBlock { () -> Void in  
    print("execution block1 -- \(NSThread.currentThread())")
}
operation.start()  

 以上代码会获得这样的执行结果:

<NSThread: 0x7f89b1c070f0>{number = 1, name = main}  
execution block1 -- <NSThread: 0x7f89b1e17030>{number = 2, name = (null)}  

 

首先咱们建立了一个NSBlockOperation,而且设置好它的 block ,也就是将要执行的任务。这个任务会在主线程中执行。

用 NSBlockOperation 是由于 NSOperation 是一个基类,不该该直接生成 NSOperation 对象,而是应该用它的子类。NSBlockOperation 是苹果预约义的子类,它能够用来封装一个或多个 block ,后面会介绍如何本身建立 NSOperation 的子类。

同时,还能够调用 addExecutionBlock 方法追加几个任务,这些任务会并行执行(也就是说颇有可能运行在别的线程里)。

最后,调用 start 方法让 NSOperation 方法运行起来。start 是一个同步方法。

刚刚咱们知道,默认的 NSOperation 是同步执行的。简单的看一下 NSOperation 类的定义会发现它有一个只读属性 asynchronous

这意味着若是想要异步执行,就须要自定义 NSOperation 的子类。或者使用 NSOperationQueue

NSOperationQueue 相似于 GCD 中的队列。咱们知道 GCD 中的队列有三种:主队列、串行队列和并行队列。NSOperationQueue 更简单,只有两种:主队列和非主队列。

咱们本身生成的 NSOperationQueue 对象都是非主队列,主队列能够用 NSOperationQueue.mainQueue 取得。

NSOperationQueue 的主队列是串行队列,并且其中全部 NSOperation 都会在主线程中执行。

对于非主队列来讲,一旦一个 NSOperation 被放入其中,那这个NSOperation 必定是并发执行的。由于 NSOperationQueue 会为每个 NSOperation 建立线程并调用它的 start 方法。

NSOperationQueue 有一个属性叫 maxConcurrentOperationCount,它表示最多支持多少个 NSOperation 并发执行。若是 maxConcurrentOperationCount 被设为 1,就觉得这个队列是串行队列

所以,NSOperationQueue 和 GCD 中的队列有这样的对应关系:

 回到开头的问题,如何利用 NSOperationQueue 实现异步操做呢,代码以下:

let operationQueue = NSOperationQueue()  
let operation = NSBlockOperation ()  
operation.addExecutionBlock { () -> Void in  
    print("exec block1 -- \(NSThread.currentThread())")
}
operation.addExecutionBlock { () -> Void in  
    print("exec block2 -- \(NSThread.currentThread())")
}
operation.addExecutionBlock { () -> Void in  
    print("exec block3 -- \(NSThread.currentThread())")
}
operationQueue.addOperation(operation)  
print("操做结束")  

 获得运行结果以下:

操做结束
exec block1 -- <NSThread: 0x125672f10>{number = 2, name = (null)}  
exec block2 -- <NSThread: 0x12556ba40>{number = 3, name = (null)}  
exec block3 -- <NSThread: 0x125672f10>{number = 2, name = (null)}  

 

使用 NSOperationQueue 来执行任务与以前的区别在于,首先建立一个非主队列。而后用 addOperation 方法替换以前的 start 方法。刚刚已经说过,NSOperationQueue 会为每个 NSOperation 创建线程并调用他们的 start 方法。

观察一下运行结果,全部的 NSOperation 都没有在主线程执行,从而成功的实现了异步、并行处理。 

在学习 NSOperation 的时候,咱们老是用GCD的概念去解释。可是 NSOperation 做为对 GCD 更高层次的封装,它有着一些 GCD 没法实现(或者至少说很难实现)的特性。因为 NSOperation 和 NSOperationQueue 良好的封装,这些新特性的使用都很是简单。

若是咱们有两次网络请求,第二次请求会用到第一次的数据。假设此时网络状况很差,第一次请求超时了,那么第二次请求也没有必要发送了。固然,用户也有可能人为地取消某个 NSOperation

 当某个 NSOperation 被取消时,咱们应该尽量的清除 NSOperation 内部的数据而且把 cancelled 和 finished 设为 true,把executing 设为 false

 

//取消某个NSOperation
operation1.cancel()

//取消某个NSOperationQueue剩余的NSOperation
queue.cencelAllOperations()

依然考虑刚刚所说的两次网络请求的例子。由于第二次请求会用到第一次的数据,因此咱们要保证发出第二次请求的时候第一个请求已经执行完。可是咱们同时还但愿利用到 NSOperationQueue 的并发特性(由于可能不止这两个任务)。

这时候咱们能够设置 NSOperation 之间的依赖关系。语法很是简洁:

operation2.addDependency(operation1)

 须要注意的是 NSOperation 之间的相互依赖会致使死锁

queue.suspended = true //暂停queue中全部operation  
queue.suspended = false //恢复queue中全部operation  

这个更加简单,只要修改 suspended 属性便可

GCD中,任务(block)是没有优先级的,而队列具备优先级。和GCD相反,咱们通常考虑 NSOperation 的优先级

NSOperation 有一个NSOperationQueuePriority 枚举类型的属性 queuePriority

public enum NSOperationQueuePriority : Int {  
    case VeryLow
    case Low
    case Normal
    case High
    case VeryHigh
}

 须要注意的是,NSOperationQueue 也不能彻底保证优先级高的任务必定先执行。

通过以上分析,咱们大概对 NSOperation 和 GCD 都有了比较详细的了解,同时在亲自运用这二者的过程当中有了本身的理解。

GCD以 block 为单位,代码简洁。同时 GCD 中的队列、组、信号量、source、barriers 都是组成并行编程的基本原语。对于一次性的计算,或是仅仅为了加快现有方法的运行速度,选择轻量化的 GCD 就更加方便。

而 NSOperation 能够用来规划一组任务之间的依赖关系,设置它们的优先级,任务能被取消。队列能够暂停、恢复。NSOperation 还能够被子类化。这些都是 GCD 所不具有的。

因此咱们要记住的是:

NSOperation 和 GCD 并非互斥的,有效地结合二者能够开发出更棒的应用

NSOperation 有本身独特的优点,GCD 也有一些强大的特性。接下来咱们由浅入深,讨论如下几个部分:

  • dispatch_suspend 和 dispatch_resume
  • dispathc_once
  • dispatch_barrier_async
  • dispatch_semaphore

咱们知道NSOperationQueue有暂停(suspend)和恢复(resume)。其实GCD中的队列也有相似的功能。用法也很是简单:

dispatch_suspend(queue) //暂停某个队列  
dispatch_resume(queue)  //恢复某个队列  

 这些函数不会影响到队列中已经执行的任务,队列暂停后,已经添加到队列中但尚未执行的任务不会执行,直到队列被恢复。

首先咱们来看一下最简单的 dispathc_once 函数,这在单例模式中被普遍使用。

  • dispathc_once 函数能够确保某个 block 在应用程序执行的过程当中只被处理一次,并且它是线程安全的。因此单例模式能够很简单的实现,以 OC 中 Manager 类为例
+ (Manager *)sharedInstance {
    static Manager *sharedManagerInstance = nil;
    static dispatch_once_t once;

    dispatch_once($once, ^{
        sharedManagerInstance = [[Manager alloc] init];
    });

    return sharedManagerInstance;
}

 这段代码中咱们建立一个值为 nil 的 sharedManagerInstance 静态对象,而后把它的初始化代码放到 dispatch_once 中完成。

这样,只有第一次调用 sharedInstance 方法时才会进行对象的初始化,之后每次只是返回 sharedManagerInstance 而已。

咱们知道数据在写入时,不能在其余线程读取或写入。可是多个线程同时读取数据是没有问题的。因此咱们能够把读取任务放入并行队列,把写入任务放入串行队列,而且保证写入任务执行过程当中没有读取任务能够执行。

这样的需求比较常见,GCD提供了一个很是简单的解决办法——dispatch_barrier_async

假设咱们有四个读取任务,在第2、三个任务之间有一个写入任务,代码大概是这样:

let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)

dispatch_async(queue, block1_for_reading)  
dispatch_async(queue, block2_for_reading)

/*
    这里插入写入任务,好比:
    dispatch_async(queue, block_for_writing)
*/

dispatch_async(queue, block3_for_reading)  
dispatch_async(queue, block4_for_reading)

 若是代码这样写,因为这几个 block 是并发执行,就有可能在前两个 block 中读取到已经修改了的数据。若是是有多写入任务,那问题更严重,可能会有数据竞争。

 若是使用 dispatch_barrier_async 函数,代码就能够这么写:

dispatch_async(queue, block1_for_reading)  
dispatch_async(queue, block2_for_reading)

dispatch_barrier_async(queue, block_for_writing)

dispatch_async(queue, block3_for_reading)  
dispatch_async(queue, block4_for_reading) 

 dispatch_barrier_async 会把并行队列的运行周期分为这三个过程:

  1. 首先等目前追加到并行队列中全部任务都执行完成 
  2. 开始执行 dispatch_barrier_async 中的任务,这时候即便向并行队列提交任务,也不会执行 
  3. dispatch_barrier_async 中的任务执行完成后,并行队列恢复正常。

总的来讲,dispatch_barrier_async 起到了“承上启下”的做用。它保证此前的任务都先于本身执行,此后的任务也迟于本身执行。正如barrier的含义同样,它起到了一个栅栏、或是分水岭的做用。

这样一来,使用并行队列和 dispatc_barrier_async 方法,就能够高效的进行数据和文件读写了。

首先介绍一下信号量(semaphore)的概念。信号量是持有计数的信号,不过这么解释等于没解释。咱们举个生活中的例子来看看。

假设有一个房子,它对应进程的概念,房子里的人就对应着线程。一个进程能够包括多个线程。这个房子(进程)有不少资源,好比花园、客厅等,是全部人(线程)共享的。

可是有些地方,好比卧室,最多只有两我的能进去睡觉。怎么办呢,在卧室门口挂上两把钥匙。进去的人(线程)拿着钥匙进去,没有钥匙就不能进去,出来的时候把钥匙放回门口。

这时候,门口的钥匙数量就称为信号量(Semaphore)。很明显,信号量为0时须要等待,信号量不为零时,减去1并且不等待。

在GCD中,建立信号量的语法以下:

var semaphore = dispatch_semaphore_create(2) 

 这句代码经过 dispatch_semaphore_create 方法建立一个信号量并设置初始值为 2。而后就能够调用 dispatch_semaphore_wait 方法了。

dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)  

 dispatch_semaphore_wait 方法表示一直等待直到信号量的值大于等于 1,当这个方法执行后,会把第一个信号量参数的值减 1。

第二个参数是一个 dispatch_time_t 类型的时间,它表示这个方法最大的等待时间。这在第一章中已经讲过,好比 DISPATCH_TIME_FOREVER 表示永久等待。

返回值也和 dispatch_group_wait 方法同样,返回 0 表示在规定的等待时间内第一个参数信号量的值已经大于等于 1,不然表示已超过规定等待时间,但信号量的值仍是 0。

dispatch_semaphore_wait 方法返回 0,由于此时的信号量的值大于等于一,任务得到了能够执行的权限。这时候咱们就能够安全的执行须要进行排他控制的任务了。

任务结束时还须要调用 dispatch_semaphore_signal() 方法,将信号量的值加 1。这相似于以前所说的,从卧室出来要把锁放回门上,不然后来的人就没法进入了。

咱们来看一个完整的例子:

var semaphore = dispatch_semaphore_create(1)  
let queue = dispatch_queue_create("com.gcd.kt", DISPATCH_QUEUE_CONCURRENT)  
var array: [Int] = []

for i in 1...100000 {  
    dispatch_async(queue, { () -> Void in
        /*
            某个线程执行到这里,若是信号量值为1,那么wait方法返回1,开始执行接下来的操做。
            与此同时,由于信号量变为0,其它执行到这里的线程都必须等待
        */
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

        /*
            执行了wait方法后,信号量的值变成了0。能够进行接下来的操做。
            这时候其它线程都得等待wait方法返回。
            能够对array修改的线程在任意时刻都只有一个,能够安全的修改array
        */
        array.append(i)

        /*
            排他操做执行结束,记得要调用signal方法,把信号量的值加1。
            这样,若是有别的线程在等待wait函数返回,就由最早等待的线程执行。
        */
        dispatch_semaphore_signal(semaphore)
    })
}
相关文章
相关标签/搜索