iOS多线程浅汇-实战篇

144
作者 nuclear
2016.03.29 11:16* 字数 9818

一、前言

上一篇文章iOS多线程浅汇-原理篇中整理了一些有关多线程的基本概念。本篇博文介绍的是iOS中常用的几个多线程技术:
NSThread
GCD
NSOperation

由于apple不提倡开发者直接对线程进行操作,日常开发过程中GCD和NSOperation的使用也较多,因此NSThread会介绍得少些,主要篇幅会放在后面两个。

二、NSThread

2.1 NSThread简介

NSThread是经过apple封装的面向对象的,它允许开发者直接以面向对象的思想对线程进行操作,没一个NSThread对象就代表一条线程,但是开发者必须手动管理线程的生命周期,这点是apple 不提倡的。如果开发者需要手动管理线程时apple提出了以下几点建议:

1、使用GCD和NSOperation

Writing thread-creation code manually is tedious and potentially error-prone and you should avoid it whenever possible. OS X and iOS provide implicit support for concurrency through other APIs

官方文档中说,由于手动管理线程很乏味,并且容易出错,所以apple建议你使用apple自家提供的GCD和NSOperation来进行多线程编程(apple心机婊,说好的手动管理线程的,怎么建议人家使用你的线程自动管理API了呢)。

2、停止长时间在idle状态的线程

remember that threads consume precious system resources.Threads use a nontrivial amount of memory, some of it wired, so releasing an idle thread not only helps reduce your application’s memory footprint, it also frees up more physical memory for other system processes to use

由于线程会占据大量的系统资源,适当的讲长时间处于挂起状态的线程停止,会释放更多的CPU和内存资源。

3、避免让多条线程同时处理一个数据

The simplest and easiest way to avoid thread-related resource conflicts is to give each thread in your program its own copy of whatever data it needs.
使每条线程都操作自己需要操作的数据,如果有必要的话将数据复制一份给另外一条线程处理。

...

如果你真的下定决心要自己管理线程,让Threading Programming Guide帮助你吧。

2.2 NSThread使用:

apple提供了两个方式来使用NSThread:

  • 使用 detachNewThreadSelector:toTarget:withObject:

    NSThread.detachNewThreadSelector(Selector("thread"), toTarget: self, withObject: nil)
  • 使用initWithTarget:selector:object:方法创建线程,但是必须手动启动:

    let thread = NSThread(target: self, selector:Selector("thread") , object: nil)
    thread.start()

    打印结果:

    2016-03-23 16:51:51.238 Mutiple[9192:3103867] NSThread-(
    "<NSThread: 0x7faf50653330>{number = 3, name = (null)}"
    )

  • 使用 performSelectorInBackground: withObject:方法创建并自动启动

遗憾的是swift无法使用这个方法,原因:

The performSelector: method and related selector-invoking methods are not imported in Swift because they are inherently unsafe.

apple认为这个方法不是类型安全的。

2.3 NSThread几个常用的方法:

//获取主线程和当前线程:
NSThread.mainThead()
NSThread.currentThread()

//退出,取消线程
NSThread.exit()
thread.cancel()

//判断线程状态
thread.executing//是否在运行
thread.finished   //是否结束
thread.cancelled //是否取消

//判断是否为多线程
NSThread.isMultiThread()

//使用通知根据线程状态做一些操作
NSDidBecomeSingleThreadedNotification//变为单线程
NSThreadWillExitNotification//线程即将结束
NSWillBecomeMultiThreadedNotification//即将变为多线程

//暂停线程

三、GCD

GCD是apple提出的为提高多核效率的多线程实现方案,

This technology takes the thread management code you would normally write in your own applications and moves that code down to the system level. All you have to do is define the tasks you want to execute and add them to an appropriate dispatch queue. GCD takes care of creating the needed threads and of scheduling your tasks to run on those threads

它在系统层级帮助开发者维护线程的生命周期,包括线程的创建、休眠、销毁等,开发者只需要关心需要实现的功能,将需要做的操作放到调度队列(dispatch queue)中,系统会根据线程的情况自动分配资源。

GCD引入了任务(task)和调度队列(dispatch queue)的概念。
所谓的任务,其实就是你要实现的功能和操作,即你要做什么;
调度队列,是实现功能的方式,简单说就是你想怎么做。

apple列举了dispatch queue的许多优点:

  • 提供了更简洁的实现多线程的接口,让代码更简洁
  • 帮开发者管理线程的生命周期
  • 异步队列中不会造成死锁
    ...

队列中的所有任务都按照FIFO的顺序执行,GCD提供了三种队列:

  • 串行队列
  • 并行队列
  • 主队列

3.1串行队列

  • 创建
    使用dispatch_queue_create(label: UnsafePointer<Int8>, attr: dispatch_queue_attr_t)方法创建队列,来看看apple对方法中的两个属性的解释:

label:
A string label to attach to the queue to uniquely identify it in debugging tools such as Instruments, sample, stackshots, and crash reports. Because applications, libraries, and frameworks can all create their own dispatch queues, a reverse-DNS naming style (com.example.myqueue) is recommended. This parameter is optional and can be NULL.

label是队列的名称,apple推荐使用com.example.myQueue的规则来命名队列,用于debug的时候追踪队列以便于调试,可以为空

attr:
In OS X v10.7 and later or iOS 4.3 and later, specify DISPATCH_QUEUE_SERIAL (or NULL) to create a serial queue or specify DISPATCH_QUEUE_CONCURRENT to create a concurrent queue. In earlier versions, you must specify NULL for this parameter.

attr用于指定是串行队列还是并行队列,如果是NULL则默认串行队列

let serialQ1 = dispatch_queue_create("com.hah.serialQ1",DISPATCH_QUEUE_SERIAL)

或者

let serialQ2 = dispatch_queue_create("com.hah.serialQ2")

串行队列中的任务会按照FIFO的顺序依次执行,在同步执行下,系统不会开启新的线程,默认在主线程下执行任务,如果是在异步执行下,系统会根据线程资源情况决定是否开启新的线程。

GCD中可以通过串行队列进行锁线程

来看看同步情况下任务的执行情况:

  • 同步串行:任务一个接着一个执行,不开启新线程;

        func syncSerial() {
          NSLog("=======>>>syncSerial<<<========")
          let q1 = dispatch_queue_create("q1",DISPATCH_QUEUE_SERIAL)
    
          dispatch_sync(q1) { () -> Void in
              NSLog("1-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("2-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("3-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("4-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("5-%@",NSThread.currentThread()]);
          }
          NSLog("=======>>>syncSerial<<<========")
      }

    打印结果:

    2016-03-23 13:38:29.521 Mutiple[8199:2165063] =======>>>syncSerial<<<========
    2016-03-23 13:38:29.522 Mutiple[8199:2165063] 1-(
    "<NSThread: 0x7f8418502320>{number = 1, name = main}"
    )
    2016-03-23 13:38:29.522 Mutiple[8199:2165063] 2-(
    "<NSThread: 0x7f8418502320>{number = 1, name = main}"
    )
    2016-03-23 13:38:29.522 Mutiple[8199:2165063] 3-(
    "<NSThread: 0x7f8418502320>{number = 1, name = main}"
    )
    2016-03-23 13:38:29.523 Mutiple[8199:2165063] 4-(
    "<NSThread: 0x7f8418502320>{number = 1, name = main}"
    )
    2016-03-23 13:38:29.523 Mutiple[8199:2165063] 5-(
    "<NSThread: 0x7f8418502320>{number = 1, name = main}"
    )
    2016-03-23 13:38:29.523 Mutiple[8199:2165063]
    =======>>>syncSerial<<<========

  • 异步串行:任务一个接着一个执行,系统根据线程资源情况决定是否开启新的线程;

        func asyncSerail() {
          NSLog("=======>>>asyncSerail<<<========")
          let q1 = dispatch_queue_create("q1",DISPATCH_QUEUE_SERIAL)
    
          dispatch_async(q1) { () -> Void in
              NSLog("1-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("2-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("3-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("4-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("5-%@",NSThread.currentThread());
          }
          NSLog("=======>>>asyncSerail<<<========")
      }

    打印结果:

    2016-03-23 13:04:26.713 Mutiple[7294:1959223] =======>>>asyncSerail<<<========
    2016-03-23 13:04:26.714 Mutiple[7294:1959223] =======>>>asyncSerail<<<========
    2016-03-23 13:04:26.715 Mutiple[7294:1959473] 1-(
    "<NSThread: 0x7faebac3c740>{number = 3, name = (null)}"
    )
    2016-03-23 13:04:26.716 Mutiple[7294:1959473] 2-(
    "<NSThread: 0x7faebac3c740>{number = 3, name = (null)}"
    )
    2016-03-23 13:04:26.716 Mutiple[7294:1959473] 3-(
    "<NSThread: 0x7faebac3c740>{number = 3, name = (null)}"
    )
    2016-03-23 13:04:26.716 Mutiple[7294:1959473] 4-(
    "<NSThread: 0x7faebac3c740>{number = 3, name = (null)}"
    )
    2016-03-23 13:04:26.716 Mutiple[7294:1959473] 5-(
    "<NSThread: 0x7faebac3c740>{number = 3, name = (null)}"
    )

    从打印可以看出,系统开启了新的线程,但是所有的任务都是在同一个线程里完成,避免在多个线程之间切换。

3.2 并行队列

并行队列,是全局队列的一种,它能处理并发任务。apple提供了四个已经创建好的并行队列,又叫全局队列(global queu),开发者只需要调用dispatch_get_global_queue( , )就能得到一个并行队列。并行队列同样按照FIFO的原则执行任务,它允许多个任务一起执行,同时执行的任务数量系统会根据情况决定。iOS5以后,apple也允许开发者自己创建并行队列。

3.2.1 获取全局队列:

调用dispatch_get_global_queue(identifier: Int, flags: UInt)方法获取全局队列:
两个参数:

identifier:
The quality of service you want to give to tasks executed using this queue. Quality-of-service helps determine the priority given to tasks executed by the queue. Queues that handle user-interactive or user-initiated tasks have a higher priority than tasks meant to run in the background.

identifier用于指定全局队列的优先级,有4种类型:

#define DISPATCH_QUEUE_PRIORITY_HIGH        2
#define DISPATCH_QUEUE_PRIORITY_DEFAULT     0
#define DISPATCH_QUEUE_PRIORITY_LOW         (-2)
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND  INT16_MIN

iOS 8以后可以通过:

QOS_CLASS_USER_INTERACTIVE,
QOS_CLASS_USER_INITIATED, 
QOS_CLASS_UTILITY
QOS_CLASS_BACKGROUND.

来设置优先级。

从字面上就能看出,优先级的高低顺序为:DISPATCH_QUEUE_PRIORITY_HIGH>DISPATCH_QUEUE_PRIORITY_DEFAULT>DISPATCH_QUEUE_PRIORITY_LOW>DISPATCH_QUEUE_PRIORITY_BACKGROUND
需要特别说一下的是,系统建议将有关磁盘的I/0操作放到DISPATCH_QUEUE_PRIORITY_BACKGROUND优先级的队列中,防止占用过多的系统资源。

flags :
Flags that are reserved for future use. Always specify 0 for this parameter.

flag默认是0

let highQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)

//默认优先级也可以这么写:let defaultQ = dispatch_get_global_queue(0, 0)
let defaultQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

let lowQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0)

let bgQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)

我写了段代码模拟了下优先级高的队列先执行的情况:

    func queuePriority() {
        let highQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
        let defaultQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)

        dispatch_async(defaultQ) { () -> Void in
            NSLog("task3 start-%@",NSThread.currentThread());
        }

        dispatch_async(highQ) { () -> Void in
            NSLog("task1 start-%@",NSThread.currentThread());
        }
    }

按理说,应该是先执行task1,因为它优先级比较高,但是log是这样的:

2016-03-24 16:12:49.308 Mutiple[3815:2019744] task3 start-(
"<NSThread: 0x14de16b40>{number = 3, name = (null)}"
)
2016-03-24 16:12:49.308 Mutiple[3815:2019743] task1 start-(
"<NSThread: 0x14dd4e7b0>{number = 4, name = (null)}"
)

优先级高的不先执行了,why?
我用优先级high和low做了实验,发现high优先级总是先执行,但是default为什么就不行呢?这个暂时还不明白。

3.2.2 手动创建并行队列

接下来手动创建一个并行队列,查看线程的创建情况:

  • 同步并行:任务一个接着一个执行,系统默认不开启新线程;

        func syncConcurrency() {
          NSLog("=======>>>syncConcurrency<<<========")
          let q1 = dispatch_queue_create("q1",DISPATCH_QUEUE_CONCURRENT)
    
          dispatch_sync(q1) { () -> Void in
              NSLog("1-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("2-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("3-%@",[NSThread .currentThread()]);
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("4-%@",NSThread.currentThread());
          }
    
          dispatch_sync(q1) { () -> Void in
              NSLog("5-%@",NSThread.currentThread());
          }
          NSLog("=======>>>syncConcurrency<<<========")
      }

    打印结果:

    2016-03-23 13:40:16.968 Mutiple[8256:2181632] =======>>>syncConcurrency<<<========
    2016-03-23 13:40:16.969 Mutiple[8256:2181632] 1-(
    "<NSThread: 0x7fad99605d70>{number = 1, name = main}"
    )
    2016-03-23 13:40:16.970 Mutiple[8256:2181632] 2-(
    "<NSThread: 0x7fad99605d70>{number = 1, name = main}"
    )
    2016-03-23 13:40:16.970 Mutiple[8256:2181632] 3-(
    "<NSThread: 0x7fad99605d70>{number = 1, name = main}"
    )
    2016-03-23 13:40:16.970 Mutiple[8256:2181632] 4-(
    "<NSThread: 0x7fad99605d70>{number = 1, name = main}"
    )
    2016-03-23 13:40:16.970 Mutiple[8256:2181632] 5-(
    "<NSThread: 0x7fad99605d70>{number = 1, name = main}"
    )
    2016-03-23 13:40:16.970 Mutiple[8256:2181632] =======>>>syncConcurrency<<<========

    从打印输出可以看出,所有任务都是在主线程中完成,因为同步执行,即使开了多个线程,下一个的任务还是要等待当前任务执行完毕才能开始执行,开多个线程之后造成资源浪费。就像iOS多线程浅汇-原理篇中讲到的,如果这个时候开了三条线程,系统就必须在三条线程之间切换,除此之外,每条线程都有自己的栈和寄存器,三条线程就有三组的栈和寄存器被复制和替换,这相对于只有一条线程来说,效率肯定是大大降低的。

  • 异步并行:多个任务一起执行,顺序不固定,但是可以通过dispatch_group和NSOperation添加依赖控制任务执行顺序。

        func asyncConcurrency() {
          NSLog("=======>>>asyncConcurrency<<<========")
          let q1 = dispatch_queue_create("q1",DISPATCH_QUEUE_CONCURRENT)
    
          dispatch_async(q1) { () -> Void in
              NSLog("1-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("2-%@",NSThread.currentThread();
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("3-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("4-%@",NSThread.currentThread());
          }
    
          dispatch_async(q1) { () -> Void in
              NSLog("5-%@",NSThread.currentThread());
          }
          NSLog("=======>>>asyncConcurrency<<<========")
      }

    打印结果:

    2016-03-23 13:30:20.987 Mutiple[8107:2109033] =======>>>asyncConcurrency<<<========
    2016-03-23 13:30:20.988 Mutiple[8107:2109033] =======>>>asyncConcurrency<<<========
    2016-03-23 13:30:20.989 Mutiple[8107:2109139] 5-(
    "<NSThread: 0x7fcca2546420>{number = 7, name = (null)}"
    )
    2016-03-23 13:30:20.989 Mutiple[8107:2109119] 3-(
    "<NSThread: 0x7fcca2733150>{number = 4, name = (null)}"
    )
    2016-03-23 13:30:20.989 Mutiple[8107:2109093] 1-(
    "<NSThread: 0x7fcca2732c90>{number = 3, name = (null)}"
    )
    2016-03-23 13:30:20.989 Mutiple[8107:2109118] 2-(
    "<NSThread: 0x7fcca253ea80>{number = 5, name = (null)}"
    )
    2016-03-23 13:30:20.990 Mutiple[8107:2109138] 4-(
    "<NSThread: 0x7fcca263a5e0>{number = 6, name = (null)}"
    )

  • 异步下创建多个串行队列实现并行效果

      func multiAsyncSerail() {
          NSLog("=======>>>asyncSerail<<<========")
          let q1 = dispatch_queue_create("q1",DISPATCH_QUEUE_SERIAL)
    
          dispatch_async(q1) { () -> Void in
              NSLog("1-%@",NSThread.currentThread());
          }
    
                  let q2 = dispatch_queue_create("q2",DISPATCH_QUEUE_SERIAL)
          dispatch_async(q2) { () -> Void in
              NSLog("2-%@",NSThread.currentThread());
          }
    
                  let q3 = dispatch_queue_create("q3",DISPATCH_QUEUE_SERIAL)
          dispatch_async(q3) { () -> Void in
              NSLog("3-%@",NSThread.currentThread());
          }
    
                  let q4 = dispatch_queue_create("q4",DISPATCH_QUEUE_SERIAL)
          dispatch_async(q4) { () -> Void in
              NSLog("4-%@",NSThread.currentThread());
          }
    
          let q5 = dispatch_queue_create("q5",DISPATCH_QUEUE_SERIAL)
          dispatch_async(q5) { () -> Void in
              NSLog("5-%@",NSThread.currentThread());
          }
          NSLog("=======>>>asyncSerail<<<========")
      }

    打印结果:

    2016-03-23 13:33:21.412 Mutiple[8150:2128078] =======>>>asyncSerail<<<========
    2016-03-23 13:33:21.413 Mutiple[8150:2128078] =======>>>asyncSerail<<<========
    2016-03-23 13:33:21.414 Mutiple[8150:2128320] 4-(
    "<NSThread: 0x7fa520413420>{number = 7, name = (null)}"
    )
    2016-03-23 13:33:21.414 Mutiple[8150:2128308] 1-(
    "<NSThread: 0x7fa52044b4d0>{number = 3, name = (null)}"
    )
    2016-03-23 13:33:21.414 Mutiple[8150:2128312] 3-(
    "<NSThread: 0x7fa520432f10>{number = 5, name = (null)}"
    )
    2016-03-23 13:33:21.414 Mutiple[8150:2128321] 5-(
    "<NSThread: 0x7fa52040beb0>{number = 6, name = (null)}"
    )
    2016-03-23 13:33:21.414 Mutiple[8150:2128311] 2-(
    "<NSThread: 0x7fa52061ff60>{number = 4, name = (null)}"
    )

小结:

使用GCD实现多线程时:

  • 同步串行和同步并行:都会按照顺序依次执行任务,且不会开启新的线程。

  • 异步串行:会开启新的线程,但是系统默认在同一线程内按照任务顺序依次执行,因为这个时候开了多个线程也要顺序执行任务,开线程只会造成资源浪费

  • 异步并行:开启多个线程,有多个任务同时执行,系统会根据每条线程的情况分配,执行顺序不固定。

  • 在异步串行情况下,可以通过创建多个串行队列,实现异步并行的功能,让多个任务同时进行。

3.3 主队列

The main dispatch queue is a globally available serial queue that executes tasks on the application’s main thread.

主队列是一个全局性的串行队列,用于执行app中主线程的任务,对run loop起作用,将隶属于run loop的事件源与队列任务的执行交织在一起(官方文档翻译过来的,太拗口了。。。)。主队列是一个对于app来说十分重要的同步点,凡是与UI和用户反馈相关的都必须交给主队列完成。这个队列是系统自动创建的,开发者只需要获取队列,并执行操作即可。

获取主线程的方法:

let mainQueue = dispatch_get_main_queue()

最经常用的:

let queue = dispatch_get_global_queue(0,0)
dispatch_async(queue,{ () -> Void in
   //在这里做一些耗时的操作
   dispatch_async(dispatch_get_main_queue(),{ () -> Void in
       //刷新UI
   })
})

3.4 队列组(dispatch group)

Dispatch groups are a way to block a thread until one or more tasks finish executing.Groups provide a useful synchronization mechanism for code that depends on the completion of other tasks

使用队列组能够实现让一个或多个任务执行完毕之后再执行下一个任务。

Another way to use dispatch groups is as an alternative to thread joins.

使用队列组将多个任务连接在一起。

用队列组可以在特定的场景下实现一些特定的功能,十分有趣:

加入有三个任务:taks1,task2,taks3。task1与task2没有什么特殊关系,task3需要等待task1和task2结束之后才能开始执行。

  • 使用队列组实现任务等待

      func dispatchGroup() {
          let group = dispatch_group_create()
          let queue1 = dispatch_get_global_queue(0, 0)
          let queue2 = dispatch_get_global_queue(0, 0)
          let queue3 = dispatch_get_global_queue(0, 0)
    
          dispatch_group_async(group, queue1) { () -> Void in
              for i in 0..<3 {
                  NSLog("task1 - %d",i)
              }
          }
    
          dispatch_group_async(group, queue2) { () -> Void in
              for i in 0..<3 {
                  NSLog("task2 - %d",i)
              }
          }
    
          dispatch_group_wait(group, DISPATCH_TIME_FOREVER)
    
          NSLog("wait completed")
    
          dispatch_group_async(group, queue3) { () -> Void in
              for i in 0..<3 {
                  NSLog("task3 - %d",i)
              }
          }
      }

    log输出:

    2016-03-24 22:23:25.717 2016-03-24 22:42:42.465 MultipleThradDemo[49112:4109966] task2 - 0
    2016-03-24 22:42:42.465 MultipleThradDemo[49112:4109967] task1 - 0
    2016-03-24 22:42:42.467 MultipleThradDemo[49112:4109966] task2 - 1
    2016-03-24 22:42:42.467 MultipleThradDemo[49112:4109967] task1 - 1
    2016-03-24 22:42:42.468 MultipleThradDemo[49112:4109966] task2 - 2
    2016-03-24 22:42:42.468 MultipleThradDemo[49112:4109967] task1 - 2
    2016-03-24 22:42:42.468 MultipleThradDemo[49112:4109901] wait completed
    2016-03-24 22:42:42.469 MultipleThradDemo[49112:4109967] task3 - 0
    2016-03-24 22:42:42.469 MultipleThradDemo[49112:4109967] task3 - 1
    2016-03-24 22:42:42.469 MultipleThradDemo[49112:4109967] task3 - 2

    dispatch_group_wait(group: dispatch_group_t, timeout:dispatch_time_t):括号里面的第一个参数group为等待的队列组,第二个timeout为等待的时间

  • 使用队列组的通知功能实现上述场景:

      func groupNotify() {
          let group = dispatch_group_create()
          let queue1 = dispatch_get_global_queue(0, 0)
          let queue2 = dispatch_get_global_queue(0, 0)
          let queue3 = dispatch_get_global_queue(0, 0)
    
          dispatch_group_async(group, queue1) { () -> Void in
              for i in 0..<3 {
                  NSLog("task1 - %d",i)
              }
          }
    
          dispatch_group_async(group, queue2) { () -> Void in
              for i in 0..<3 {
                  NSLog("task2 - %d",i)
              }
          }
    
          dispatch_group_notify(group, queue3) { () -> Void in
              NSLog("task1、 task2 completed")
              for i in 0..<3 {
                  NSLog("task3 - %d",i)
              }
          }
      }

    log输出:

    2016-03-24 23:05:29.010 MultipleThradDemo[49493:4127151] task2 - 0
    2016-03-24 23:05:29.010 MultipleThradDemo[49493:4127148] task1 - 0
    2016-03-24 23:05:29.011 MultipleThradDemo[49493:4127148] task1 - 1
    2016-03-24 23:05:29.011 MultipleThradDemo[49493:4127151] task2 - 1
    2016-03-24 23:05:29.011 MultipleThradDemo[49493:4127148] task1 - 2
    2016-03-24 23:05:29.011 MultipleThradDemo[49493:4127151] task2 - 2
    2016-03-24 23:05:29.012 MultipleThradDemo[49493:4127151] task1、 task2 completed
    2016-03-24 23:05:29.012 MultipleThradDemo[49493:4127151] task3 - 0
    2016-03-24 23:05:29.012 MultipleThradDemo[49493:4127151] task3 - 1
    2016-03-24 23:05:29.012 MultipleThradDemo[49493:4127151] task3 - 2

3.5 GCD的线程安全

尽管GCD本身就是线程安全的,但是为了更好的使用GCD,apple还是给了几个建议:

Do not call the dispatch_sync function from a task that is executing on the same queue that you pass to your function call

在本队列调用dispatch_sync,再将本队列传入同步方法会造成死锁。

我想apple 的意思应该是这样的:

    func deadLock() {
    //let q = dispatch_get_global_queue(0, 0)
        let q = dispatch_queue_create("come.multiThread.serialQ", DISPATCH_QUEUE_SERIAL)

        dispatch_async(q) { () -> Void in
            NSLog("1 start-%@",[NSThread .currentThread()]);
            dispatch_sync(q, { () -> Void in
                NSLog("2 start-%@",[NSThread .currentThread()]);
            })
            NSLog("3 start-%@",[NSThread .currentThread()]);
        }
        NSLog("4 start-%@",[NSThread .currentThread()]);
    }

但是我发现会不会造成死锁和当前队列是串行还是并行有关。
如果是串行队列,同步执行会阻塞线程,而且串行队列一次只能执行一个任务,q里面等待的任务与同步阻塞的任务是同一个任务,造成死锁。
如果是并行队列,一次能执行多个任务,即使同步阻塞了线程,由于可以多个任务一起执行,q里面没有任务等待,不会造成死锁。

stackoverflow上类似问题的回答:

Using dispatch_sync() to enqueue a Block to the serial queue on which you're already running is guaranteed to deadlock.Using dispatch_sync() to enqueue a Block to the concurrent queue on which you're already running is quite likely to deadlock.

派发同步任务到在正在执行的串行队列肯定会造成死锁;派发到并行队列也有很大的可能造成死锁。所以上面的例子中,并行队列虽然没有造成死锁,还是有风险的。

note:往主线程派发同步任务也会造成死锁

    func mainThreadLock() {
        let main = dispatch_get_main_queue()
        NSLog("start-%@",[NSThread .currentThread()]);
        dispatch_sync(main) { () -> Void in
            NSLog("1 -%@",[NSThread .currentThread()]);
        }
        NSLog("end-%@",[NSThread .currentThread()]);
    }

打印输出:

2016-03-25 10:15:46.638 Mutiple[15813:5558093] start-(
"<NSThread: 0x7fae226029d0>{number = 1, name = main}"
)

同步任务与主线程任务相互等待,造成死锁。

Avoid taking locks from the tasks you submit to a dispatch queue.If you need to synchronize parts of your code, use a serial dispatch queue instead of a lock.

尽量不用自己锁线程,否则得到的结果可能会十分出乎你的意料。使用GCD实现多线程时,apple建议用串行队列替代线程锁。

3.6 GCD的其他常用方法

3.6.1 dispatch_group_enter

dispatch_group_enter()dispatch_group_leave()是用于进入group和退出group的指示方法,是用于手动管理group的block块的方法,下面两段代码的功能是相同的:

let group = dispatch_group_create()
let queue = dispatch_get_global_queue(0,0)
//实现异步操作
dispatch_group_enter(group)
dispatch_async(queue,^{
    //异步操作
    dispatch_group_leave(group)
})

//等价于
dispatch_group_async(group,queue,^{
    //异步操作
})
3.6.1 dispatch semaphore

dispatch semaphore 信号量用于控制同时访问同一个资源的任务数量。当一个并行任务中需要对资源数据做更小部分的控制或者多个线程存在竞争的时候,信号量就变得十分有用。
信号量主要有三个方法:

/*!
  *@function
  *创建信号量
  *
  *@description
  *value=0:两个线程需要同时完成
  *value<0:返回NULL
  *
  *@param value
  *用于指定开始的时候的信号量的个数
 */
func dispatch_semaphore_create(value:Int) -> dispatch_semaphore_t!

/*!
  *@function
  *等待信号,并使得create方法中的value值-1
  *
  *@description
  *返回值>=0时使得信号量减一并继续执行以下代码,<0会等待信号量发送后再返回返回值
  *
  *@param dsema
  *等待的信号
  *
  *@param timeout
  *等待的时间
  *@result
  *0:成功,非零:等待
 */
func dispatch_semaphore_wait(dsema: dispatch_semaphore_t,timeout: dispatch_time_t) -> Int

/*!
  *@function
  *使得信号量增加
  *
  *@description
  *如果要返回的值<0,会先唤醒一个等待线程再返回值
  *
  *@param dsema
  *等待的信号
  *
  *@result
  *0:未能唤醒线程,非零:唤醒线程
 */
func dispatch_semaphore_signal(dsema:dispatch_semaphore_t) -> Int

看一个例子:

func changeMutableArray() {
    var arr = [Int]()
    let queue1 = dispatch_get_global_queue(0, 0)

    for i in 0...3 {
        dispatch_async(queue1) { () -> Void in
            NSLog("task2 - %d",i)
            arr.append(i)
            NSLog("\(arr)")
        }
    }
}

在两个异步线程中对一个可变数组做添加元素操作,由于可变数组不支持多线程操作,当两个线程同时访问它时就会造成数据混乱,我得出了这样的打印:

2016-03-28 16:44:34.193 Mutiple[5669:1717543] task2 - 1
2016-03-28 16:44:34.193 Mutiple[5669:1717545] task2 - 2
2016-03-28 16:44:34.193 Mutiple[5669:1717544] task2 - 0
2016-03-28 16:44:34.193 Mutiple[5669:1717554] task2 - 3
2016-03-28 16:44:34.194 Mutiple[5669:1717544] [1, 2, 0]
2016-03-28 16:44:34.194 Mutiple[5669:1717554] [1, 2, 0, 3]
2016-03-28 16:44:34.194 Mutiple[5669:1717545] [1, 2, 0]
2016-03-28 16:44:34.194 Mutiple[5669:1717543] [1, 2]

加上了信号量的保护之后,代码是这样的

func changeMutableArray() {
    var arr = [Int]()
    let queue1 = dispatch_get_global_queue(0, 0)

    let semaphore = dispatch_semaphore_create(1)

    for i in 0...3 {
        dispatch_async(queue1) { () -> Void in

            dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)

            NSLog("task2 - %d",i)
            arr.append(i)
            NSLog("\(arr)")

            dispatch_semaphore_signal(semaphore)
        }
    }
}

刚开始创建的时候定义了初始的计数器的值为1,dispatch_semaphore_wait()在执行前会先判断当前的counter值,如果counter值大于等于1,会往下执行代码,并且使得counter值减1,如果counter的值小于1,等待dispatch_semaphore_signal()方法执行,该方法会使得counter加1,当counter大于等于1 wait方法才会继续执行以下的代码,

打印出来的log 信息是这样的:

2016-03-28 16:46:54.272 Mutiple[5697:1733417] task2 - 0
2016-03-28 16:46:54.273 Mutiple[5697:1733417] [0]
2016-03-28 16:46:54.273 Mutiple[5697:1733416] task2 - 1
2016-03-28 16:46:54.273 Mutiple[5697:1733416] [0, 1]
2016-03-28 16:46:54.274 Mutiple[5697:1733440] task2 - 2
2016-03-28 16:46:54.274 Mutiple[5697:1733440] [0, 1, 2]
2016-03-28 16:46:54.274 Mutiple[5697:1733463] task2 - 3
2016-03-28 16:46:54.274 Mutiple[5697:1733463] [0, 1, 2, 3]

可以看出,信号量在竞争添加下保证了往数组添加元素的安全性。

除此之外信号量还可以这样用

let result = dispatch_semaphore_wait(dsema,DISPATCH_TIME_FORVEER)
if result > 0 {
    //做异步操作
}else{
    //等待signal方法增加counter值
}

NOTE:如果wait和signal不匹配出现可能会崩溃。

3.6.2 dispatch barrier

dispatch barrier允许开发者在一个并行队列中创造一个同步点,其实也是用于在竞争条件下资源的保护,防止同一资源同时被使用。在并行队列中,barrier中的代码会等到该队列的所有并行条件结束之后才会单独执行barrier。

barrier两个常用方法:

/*
 *@discussion
 *将barrier代码块提交到队列中,然后马上返回。
 *等待队列执行完barrier前面的所有代码之后,barrier里面的代码会单独执行,
 *等到barrier代码执行完毕,队列继续往下执行。
 */
func dispatch_barrier_async(queu: dispatch_queue_t, block: dispatch_block_t)

note:这个队列应该使用自己创建的队列,如果传入的是一个串行队列或者全局队列,那么这个方法和dsipatch_async的功能是一样的。

/*
 *@discussion
 *提交barrier到队列,等待barrier的block执行完毕再返回。
 *这个方法会等待block执行完毕才会返回。
 *同dispatch_barrier_async,队列需要使用自己创建的私人队列。
 */
func dispatch_barrier_sync(queue:dispatch_queue_t, block: dispatch_block_t)

note: 在当前队列中调用此方法会造成死锁, 系统倾向于当前线程中(而不是再开一条线程)调用此方法使性能最优。

dispatch_barrier_async传入自己创建的并行队列时,会阻塞当前队列执行,而不阻塞当前线程。
dispatch_barrier_sync传入自己创建的并行队列时,会阻塞当前线程,所有在正在执行的队列中调用次方法会使队列任务相互等待,造成死锁。

用代码试验一下:

func asyncBarrier() {
    let queue = dispatch_queue_create("com.multiThread.barrierQ", DISPATCH_QUEUE_CONCURRENT)

    dispatch_async(queue) { () -> Void in
        for i in 0...2 {
            NSLog("task1 - %d",i)
        }
    }

    dispatch_barrier_async(queue) { () -> Void in
        NSLog("=====>>>barrier1 start <<<======")
        for i in 0...2 {
            NSLog("task2 - %d",i)
        }
        NSLog("=====>>>barrier1 completed <<<======")
    }

    dispatch_async(queue) { () -> Void in
        for i in 0...2 {
            NSLog("task4 - %d",i)
        }
    }
}

打印输出:

2016-03-29 11:11:33.496 Mutiple[7756:2543177] task1 - 0
2016-03-29 11:11:33.496 Mutiple[7756:2543177] task1 - 1
2016-03-29 11:11:33.496 Mutiple[7756:2543177] task1 - 2
2016-03-29 11:11:33.497 Mutiple[7756:2543177] =====>>>barrier1 start <<<======
2016-03-29 11:11:33.497 Mutiple[7756:2543177] task2 - 0
2016-03-29 11:11:33.497 Mutiple[7756:2543177] task2 - 1
2016-03-29 11:11:33.497 Mutiple[7756:2543177] task2 - 2
2016-03-29 11:11:33.497 Mutiple[7756:2543177] =====>>>barrier1 completed <<<======
2016-03-29 11:11:33.497 Mutiple[7756:2543177] task4 - 0
2016-03-29 11:11:33.497 Mutiple[7756:2543177] task4 - 1
2016-03-29 11:11:33.498 Mutiple[7756:2543177] task4 - 2

造成死锁的情况:

func syncBarrier() {
    let queue = dispatch_queue_create("com.multiThread.barrierQ", DISPATCH_QUEUE_CONCURRENT)
    dispatch_async(queue) { () -> Void in
        NSLog("sync barrier start")
        dispatch_barrier_sync(queue) { () -> Void in
            NSLog("=====>>>barrier1 start <<<======")
            for i in 0...2 {
                NSLog("task2 - %d",i)
            }
            NSLog("=====>>>barrier1 completed <<<======")
        }
        NSLog("sync barrier completed")
    }
}

只有一条打印信息:

2016-03-29 11:15:03.510 Mutiple[7851:2563610] sync barrier start

3.6.3 dispatch source

dispatch source是一个监视某些类型事件的对象。当这些事件发生时,它自动将一个block放入一个dispatch queue的执行例程中。
GCD支持以下的dispatch source类型:

  • Timer dispatch sources
  • Signal dispatch sources
  • Descriptor sources
  • Process dispatch sources
  • Mach port dispatch sources
  • Custom dispatch sources

dispatch sources 用一些异步回调方法处理系统相关的事件。不像之前手动的将任务提交到dispatch queue,dispatch source为app提供了持续的事件源,在你取消它之前它都会一直连接这dispatch queue,等待着事件的触发(有点像通知的感觉)

  • 创建source

    //dispatch source 一直处于挂起状态,创建source,配置了handler和context之后,需要调用dispatch_resume()方法
    public func dispatch_source_create(type: dispatch_source_type_t, 
                                   _ handle: UInt, _ mask: Uint,
                                    _ queue: dispatch_queue_t!) -> dispatch_source_t!
  • 配置事件

    dispatch_source_set_event_handle(source:dispatch_source_t, handler: dispatch_block_t!)

文档中的示例代码:

dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ,
                                 myDescriptor, 0, myQueue);
dispatch_source_set_event_handler(source, ^{
   // Get some data from the source variable, which is captured
   // from the parent context.
   size_t estimated = dispatch_source_get_data(source);

   // Continue reading the descriptor...
});
dispatch_resume(source);

四、NSOeration

NSOperation 是apple 提供的面向对象的多线程实现方案。NSOperationNSOperationQueue分别对应GCD中的task和queue

NSOperation是一个抽象类,不能直接创建对象封装任务,可以使用它的两个子类NSInvocationOperationNSBlockOperation或者自己创建继承自NSOperation的类对象封装任务。

4.1 NSInvocation的使用:

NSInvocation *invocation = [[NSInvocation alloc] initWithTarget:aTarget 
                                                       selector:@selector(aSelector) 
                                                         object:nil];
[invocation start];

NOTE:Swift中认为NSInvocationOperation是非类型安全的,所有不能使用

4.2 NSBlockOperation

NSBlockOperation可以并行执行一个或多个任务,开发者只需要将任务放到block里面。在创建NSBlockOperation对象的时候,你就要应该初始化至少一个block。这个operation会等到所有的block都执行完毕才会结束。finished属性是一个布尔值,可以监测blockOperation是否执行完毕。将一组的任务添加到block,然后监测finished就能追踪一组任务的完成情况。

基本用法

func blockOperation() {

    let theOP = NSBlockOperation { () -> Void in
        for i in 0...2 {
            NSLog("task1 - %d",i)
        }
    }
    theOP.addExecutionBlock { () -> Void in
        for i in 0...2 {
            NSLog("task2 - %d",i)
        }
    }
    //不调用start方法不会开始执行
    theOP.start()
}

4.3 自定义operation子类

当系统提供的两个NSOperation的子类无法满足咱们的需求的时候,咱们就要创建继承自NSOperation的对象来实现更多更复杂的操作。
自己创建的子类对象必要实现两个方法:

  • 自定义的初始化方法
  • 重写main方法。在这个方法里面所需要的操作,一般会建立自己的自动释放池,原因:异步执行的时候无法访问主线程的释放池,不会及时释放,可能造成资源浪费
@interface MyNonConcurrentOperation : NSOperation
@property (strong) id myData;
-(id)initWithData:(id)data;
@end

@implementation MyNonConcurrentOperation
- (id)initWithData:(id)data {
   if (self = [super init])
      myData = data;
   return self;
}

-(void)main {
    @autoreleasepool{
        //执行操作
    }
}
@end

4.4 让NSOperation实现并行的几个方法

operation对象默认以同步方式执行任务,哪条线程调用了start方法,就会在哪条线程上执行任务。尽管大多数operation是异步运行的,operation队列只提供非并行操作的线程。如果开发者要实现真正的并行操作,可以通过重写以下几个方法:

  • start(Required)
    所有的并发操作都必须重写这个方法,用自己的实现方式替代默认的。执行操作时,调用start方法,作为执行任务的起始点,在任何时候都不应该调用 superstart方法

  • main(Optional)
    主要用于实现与operation对象相关的操作,尽管你可以在start方法中执行任务,但是将执行任务的代码放到这个方法中,会使得配置代码与任务代码分离得更加清晰些。

  • isExcuting & isFinished(Required)
    用于向外部报告任务的执行情况,实现这两个方法时需要保证线程安全。

  • isConcurrent(Required)
    标记一个operation是否是并行操作,只需要重写这个方法 返回YES

简单实现功能:

@interface MyOperation : NSOperation {
    BOOL        executing;
    BOOL        finished;
}
- (void)completeOperation;
@end

@implementation MyOperation
- (id)init {
    self = [super init];
    if (self) {
        executing = NO;
        finished = NO;
    }
    return self;
}

- (void)start {
   // Always check for cancellation before launching the task.
   if ([self isCancelled])
   {
      // Must move the operation to the finished state if it is canceled.
      [self willChangeValueForKey:@"isFinished"];
      finished = YES;
      [self didChangeValueForKey:@"isFinished"];
      return;
   }

   // If the operation is not canceled, begin executing the task.
   [self willChangeValueForKey:@"isExecuting"];
   [NSThread detachNewThreadSelector:@selector(main) toTarget:self withObject:nil];
   executing = YES;
   [self didChangeValueForKey:@"isExecuting"];
}

- (void)main {
   @autoreleasepool {
       [self completeOperation];
   }
}

- (void)completeOperation {
    [self willChangeValueForKey:@"isFinished"];
    [self willChangeValueForKey:@"isExecuting"];

    executing = NO;
    finished = YES;

    [self didChangeValueForKey:@"isExecuting"];
    [self didChangeValueForKey:@"isFinished"];
}

- (BOOL)isConcurrent {
    return YES;
}

- (BOOL)isExecuting {
    return executing;
}

- (BOOL)isFinished {
    return finished;
}
@end

4.5 NSOpreationQueue

同GCD种的dispatch queue一样,operation queue由系统控制,开发者只需要把任务提交给它,系统会自动判断决定同时运行的操作个数,多创建一个queue并不意味着你可以同时执行更多的操作。

//创建一个operation queue
let OPQueue = NSOperationQueue()

//添加操作
[aQueue addOperation:anOp]; // Add a single operation
//添加操作组,并决定是否等待组内操作完成再继续往下执行
[aQueue addOperations:anArrayOfOps waitUntilFinished:NO]; // Add multiple operations
//直接添加操作到block中
[aQueue addOperationWithBlock:^{
   /* Do something. */
}];

Important: Never modify an operation object after it has been added to a queue. While waiting in a queue, the operation could start executing at any time, so changing its dependencies or the data it contains could have adverse effects. If you want to know the status of an operation, you can use the methods of the NSOperation class to determine if the operation is running, waiting to run, or already finished.

apple建议开发者在operation对象被提交到队列后就不要去更改operation对象了,因为它随时会开始执行,如果需要知道operation的状态,通过NSOperation的方法来确定状态。

使用操作队列的几个方法实现任务等待:

func operationQueue() {
    let OP1 = NSBlockOperation { () -> Void in
        for i in 0...2 {
            NSLog("task1 - %d",i)
        }
    }
    //OP2执行完成之后的操作
    OP1.completionBlock = { () in
        NSLog("===>>task1 completed<<===")
    }

    let OP2 = NSBlockOperation { () -> Void in
        for i in 0...2 {
            NSLog("task2 - %d",i)
        }
    }

    //添加依赖,OP1执行完毕之后才会执行OP2
    OP2.addDependency(OP1)
    //OP2执行完成之后的操作
    OP2.completionBlock = { () in
        NSLog("===>>task2 completed<<===")
    }

    let OP3 = NSBlockOperation { () -> Void in
        for i in 0...2 {
            NSLog("task3 - %d",i)
        }
    }

    let opQ = NSOperationQueue()
    opQ.addOperations([OP1,OP2], waitUntilFinished: true)
    opQ.addOperation(OP3)
}

log信息:

2016-03-30 17:05:43.303 Mutiple[2363:858459] task1 - 0
2016-03-30 17:05:43.303 Mutiple[2363:858459] task1 - 1
2016-03-30 17:05:43.304 Mutiple[2363:858459] task1 - 2
2016-03-30 17:05:43.304 Mutiple[2363:858459] ===>>task1 completed<<===
2016-03-30 17:05:43.304 Mutiple[2363:858460] task2 - 0
2016-03-30 17:05:43.305 Mutiple[2363:858460] task2 - 1
2016-03-30 17:05:43.305 Mutiple[2363:858460] task2 - 2
2016-03-30 17:05:43.305 Mutiple[2363:858460] ===>>task2 completed<<===
2016-03-30 17:05:43.305 Mutiple[2363:858459] task3 - 0
2016-03-30 17:05:43.307 Mutiple[2363:858459] task3 - 1
2016-03-30 17:05:43.308 Mutiple[2363:858459] task3 - 2

operation queue通过setMaxConcurrentOperationCount:1来实现串行队列的功能,但是执行的顺序会根据其他因素来确定,这一点与串行队列又有点不太一样。

If the execution order of your operation objects is important to you, you should use dependencies to establish that order before adding your operations to a queue

如果需要自己决定执行顺序,使用依赖来实现。

初次之外,operation que还可以暂停operation,通过一个布尔值suspended的真和假来决定是否让operation挂起。

suspended = YES,队列不会开启新的operation,但是正在执行的操作会继续运行不会被暂停也不会被取消。这个时候可以继续往队列中添加操作,这些操作同样会处于挂起状态,直到suspended = NO。操作只会在结束执行的时候被移除,可以通过KVO观察这个值的变化做相应的操作。

阅读原文

iOS随笔