iOS多线程---GCD

一、GCD简介

1. GCD概念

Grand Central Dispatch(GCD)是苹果封装的一套C的API,异步执行任务的技术之一。它可以自动管理线程的创建,调度和销毁等功能,无需开发者自己实现。
苹果官方对GCD的说明:开发者只需要定义想执行的任务并追加到适当的Dispatch Queue中,GCD就能生成必要的线程并计划执行任务。

  • 用于管理追加的Block的C语言层实现的FIFO队列
  • Atomic函数中实现的用于排他控制的轻量级信号
  • 用于管理线程的C语言层实现的一些容器

2. GCD的优点

  • GCD可用于多核的并行运算
  • GCD会自动利用更多的CPU内核(比如双核、四核)
  • GCD会自动管理线程的生命周期(创建线程、调度任务、销毁线程)
  • 程序员只需要告诉GCD想要执行什么任务,不需要编写任何线程管理代码

二、GCD任务和队列

任务:就是执行操作的意思,换句话说就是你在线程中执行的那段代码。在GCD中是放在block中的,执行任务有两种方式:同步执行(sync)异步执行(async)。两者的主要区别是:是否等待队列的任务执行结束,以及是否具备开启新线程的能力。

  • 同步执行(sync):
    • 同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行。
    • 只能在当前线程中执行任务,不具备开启新线程的能力。
  • 异步执行(async):
    • 异步添加任务到指定的队列中,它不会做任何等待,可以继续执行任务。
    • 可以在新的线程中执行任务,具备开启新线程的能力。

举个简单例子:你要打电话给小明和小白。
同步执行就是,你打电话给小明的时候,不能同时打给小白,等到给小明打完了,才能打给小白(等待任务执行结束)。而且只能用当前的电话(不具备开启新线程的能力)。
而异步执行就是,你打电话给小明的时候,不等和小明通话结束,还能直接给小白打电话,不用等着和小明通话结束再打(不用等待任务执行结束)。除了当前电话,你还可以使用其他所能使用的电话(具备开启新线程的能力)。

注意: 异步执行(async) 虽然具有开启新线程的能力,但是并不一定开启新线程。这跟任务所指定的队列类型有关。

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务。队列的结构可参考下图:
队列(Dispatch Queue)

在GCD中有两种队列:串行队列并发队列。两者都符合FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

  • 串行队列(Serial Dispatch Queue):
    • 每次只有一个任务被执行。让任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
  • 并发队列(Concurrent Dispatch Queue):
    • 可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)

注意:并发队列的并发功能只有在异步(dispatch_async)函数下才有效

两者具体区别如下两图所示:
串行队列
并发队列

三、GCD使用步骤

1. 创建方法

  1. 创建一个队列(串行队列或并发队列)
  2. 将任务追加到任务的等待队列中,然后系统就会根据任务类型执行任务(同步执行或异步执行)

创建方法 DispatchQueue.init(label: String, qos: DispatchQoS = .unspecified, attributes: DispatchQueue.Attributes = [], autoreleaseFrequency: DispatchQueue.AutoreleaseFrequency = .inherit, target: DispatchQueue? = nil)

  • label 队列的标识符,方便调试
  • qos 用于指定队列的优先级
  • attributes 队列的属性
  • autoreleaseFrequency 自动释放频率。有些队列是会在履行完任务后自动释放的,有些比如Timer等是不会自动释放的,是需要手动释放。
  • 串行队列创建
// 串行队列创建
let queue = DispatchQueue(label: "test.queue")
指定队列优先级
let queue = DispatchQueue(label: "test.queue", qos: .userInitiated)

.userInteractive //用户交互(跟主线程一样)
.userInitiated   //用户期望优先级(不要放太耗时的操作)
.default         //默认的
.utility         //公共的
.background      //后台
.unspecified     //不指定

优先级, 由上往下依次降低
  • 并行队列创建
let queue = DispatchQueue(label: "test.queue", qos: .userInitiated, attributes: .concurrent)
  • 对于串行队列,GCD提供了一种特殊的串行队列:主队列(Main Dispatch Queue).

    • 所有放置主队列中的任务,都会放到主线程中执行。
    • 可使用DispatchQueue.main获得主队列
  • 对于并发队列,GCD默认提供了全局并发队列(Global Dispatch Queue)

    • 可以使用DispatchQueue.global()来获取。

2. GCD的基本使用

1. 并发队列 + 同步执行

在当前线程中执行任务,不会开启新线程,执行完一个任务,在执行下一个任务

private func syncConCurrent() {
        print("syncConCurrent --- begin")
        
        let queue = DispatchQueue(label: "test.queue", attributes: .concurrent)
        queue.sync {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.sync {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        queue.sync {
            for _ in 0..<2 {
                print("3------\(Thread.current)")
            }
        }
    }

输出结果:
syncConCurrent — begin
1------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017008c0>{number = 1, name = main}

说明:

  • 所有任务都是在主线程中执行的(同步执行不具备开辟新线程的能力)。
  • 由于只有一个线程,所以任务只能一个一个执行(同步任务需要等待队列的任务执行结束)。

2. 并发队列 + 异步执行

可以开启多个线程,任务交替(同时)执行。

private func asyncConcurrent() {
        print("asyncConcurrent --- begin")
        
        let queue = DispatchQueue(label: "test.asyncqueue", attributes: .concurrent)
        queue.async {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.async {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        queue.async {
            for _ in 0..<2 {
                print("3------\(Thread.current)")
            }
        }
        print("asyncConcurrent---end")
    }

输出结果:
syncConCurrent — begin
asyncConcurrent—end
1------<NSThread: 0x600001799780>{number = 9, name = (null)}
2------<NSThread: 0x60000174ce00>{number = 8, name = (null)}
2------<NSThread: 0x60000174ce00>{number = 8, name = (null)}
3------<NSThread: 0x600001704940>{number = 10, name = (null)}
1------<NSThread: 0x600001799780>{number = 9, name = (null)}
3------<NSThread: 0x600001704940>{number = 10, name = (null)}

说明

  • 除了主线程,又开启了3个线程,并且任务是交替着同时执行的(异步执行具备开辟新线程的能力。并发队列可开辟多个线程,同时执行多个任务)。
  • 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始异步执行(异步执行不做等待,可以继续执行任务)。

3. 串行队列 + 同步执行

不会开启新线程,在当前线程执行任务。任务是串行的,执行完一个任务,再执行下一个任务。

private func syncSerial() {
        print("syncSerial --- begin")
        
        let queue = DispatchQueue(label: "test.syncSerial")
        queue.sync {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.sync {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        queue.sync {
            for _ in 0..<2 {
                print("3------\(Thread.current)")
            }
        }
        print("syncSerial---end")
    }

输出结果
syncSerial — begin
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
3------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
syncSerial—end

说明:

  • 所有任务都是在主线程中执行的,并没有开启新的线程。
  • 由于串行队列,所以按顺序一个一个执行

4. 串行队列 + 异步执行

可以开启多个线程,任务交替(同时)执行。

 private func asyncSerial() {
        print("asyncSerial --- begin")
        
        let queue = DispatchQueue(label: "test.asyncSerial")
        queue.async {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.async {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        queue.async {
            for _ in 0..<2 {
                print("3------\(Thread.current)")
            }
        }
        print("asyncSerial---end")
    }

输出结果
asyncSerial — begin
asyncSerial—end
1------<NSThread: 0x60000174c700>{number = 8, name = (null)}
1------<NSThread: 0x60000174c700>{number = 8, name = (null)}
2------<NSThread: 0x60000174c700>{number = 8, name = (null)}
2------<NSThread: 0x60000174c700>{number = 8, name = (null)}
3------<NSThread: 0x60000174c700>{number = 8, name = (null)}
3------<NSThread: 0x60000174c700>{number = 8, name = (null)}

说明

  • 开启了一条新线程,但是任务还是串行,所以任务是一个一个执行。
  • 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的。说明任务不是马上执行,而是将所有任务添加到队列之后才开始同步执行

5. 主队列 + 同步执行

同步执行 + 主队列在不同线程中调用结果也是不一样,在主线程中调用会出现死锁,而在其他线程中则不会。

private func syncMain() {
        print("syncMain --- begin")
        
        let queue = DispatchQueue.main
        queue.sync {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.sync {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        print("syncMain---end")
    }

互相等待崩溃

说明:

  • 在主线程中使用同步执行 + 主队列,追加到主线程的任务1、任务2、任务3都不再执行了,而且syncMain—end也没有打印,在XCode上还会报崩溃。
    在主线程中执行syncMain方法,相当于把syncMain任务放到了主线程的队列中。而同步执行会等待当前队列中的任务执行完毕,才会接着执行。那么当我们把任务1追加到主队列中,任务1就在等待主线程处理完syncMain任务。而syncMain任务需要等待任务1执行完毕,才能接着执行。
    那么,现在的情况就是syncMain任务和任务1都在等对方执行完毕。这样大家互相等待,所以就卡住了,所以我们的任务执行不了。
  • 在其他线程中使用同步执行 + 主队列:
    syncMain 任务放到了其他线程里,而任务1、任务2、任务3都在追加到主队列中,这三个任务都会在主线程中执行。syncMain 任务在其他线程中执行到追加任务1到主队列中,因为主队列现在没有正在执行的任务,所以,会直接执行主队列的任务1,等任务1执行完毕,再接着执行任务2、任务3。所以这里不会卡住线程。

6. 主队列 + 异步执行

只在主线程中执行任务,执行完一个任务,再执行下一个任务。

private func asyncMain() {
        print("asyncMain --- begin")
        
        let queue = DispatchQueue.main
        queue.async {
            for _ in 0..<2 {
                print("1------\(Thread.current)")
            }
        }
        queue.async {
            for _ in 0..<2 {
                print("2------\(Thread.current)")
            }
        }
        print("asyncMain---end")
    }

输出结果
syncMain — begin
syncMain—end
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
1------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}
2------<_NSMainThread: 0x6000017048c0>{number = 1, name = main}

说明:

  • 所有任务都是在当前线程(主线程)中执行的,并没有开启新的线程(虽然异步执行具备开启线程的能力,但因为是主队列,所以所有任务都在主线程中)。
  • 所有任务是在打印的syncConcurrent—begin和syncConcurrent—end之后才开始执行的(异步执行不会做任何等待,可以继续执行任务)。
  • 任务是按顺序执行的(因为主队列是串行队列,每次只有一个任务被执行,任务一个接一个按顺序执行)。

7. 全局队列+ 异步执行

private func asyncGloba() {
        let queue = DispatchQueue.global()
        for i in 0..<10 {
            queue.async {
                print("asyncGloba------\(Thread.current) \(i)")
            }
        }
    }

输出结果
asyncGloba------<NSThread: 0x600001704480>{number = 4, name = (null)} 6
asyncGloba------<NSThread: 0x60000177b040>{number = 11, name = (null)} 8
asyncGloba------<NSThread: 0x600001704480>{number = 4, name = (null)} 9
asyncGloba------<NSThread: 0x600001791f00>{number = 9, name = (null)} 4
asyncGloba------<NSThread: 0x60000174a180>{number = 5, name = (null)} 1
asyncGloba------<NSThread: 0x60000174ab40>{number = 3, name = (null)} 0
asyncGloba------<NSThread: 0x60000177ad80>{number = 8, name = (null)} 2
asyncGloba------<NSThread: 0x600001789600>{number = 7, name = (null)} 3
asyncGloba------<NSThread: 0x600001788bc0>{number = 10, name = (null)} 7
asyncGloba------<NSThread: 0x60000177aa80>{number = 6, name = (null)} 5

说明:

  • 说明是异步执行,没有马上执行,并且有开子线程执行

四、GCD的其他方法

1. GCD栅栏方法:dispatch_barrier_async

dispatch_barrier_async

  • 在执行完栅栏前面的操作之后,才执行栅栏操作,最后再执行栅栏后边的操作。

2. GCD延时执行方法:DispatchQueue.main.asyncAfter

  • asyncAfter函数并不是在指定时间之后才开始执行处理,而是在指定时间之后将任务追加到主队列中。严格来说,这个时间并不是绝对准确的,但想要大致延迟执行任务,asyncAfter函数是很有效的。

3. GCD一次性代码(只执行一次):dispatch_once

  • 在swift中已经废弃了,swift中单例public static let shared = ClassName()
  • 我们在创建单例、或者有整个程序运行过程中只执行一次的代码时,我们就用到了 GCD 的 dispatch_once 函数。使用
    dispatch_once 函数能保证某段代码在程序运行过程中只被执行1次,并且即使在多线程的环境下,dispatch_once也可以保证线程安全。

4. GCD队列的循环、挂起、恢复

1. dispatch_apply

  • 通常我们会用 for 循环遍历,但是 GCD 给我们提供了快速迭代的函数dispatch_apply。按照dispatch_apply指定的次数将指定的任务追加到指定的队列中,并等待全部队列执行结束。
  • 在swift使用下面方法
public class func concurrentPerform(iterations: Int, execute work: (Int) -> Swift.Void)

本来循环执行就是为了节约时间的嘛,所以默认就是用了并行队列。

private func dispatchApply() {
        print("DispatchApply Begin")
        DispatchQueue.concurrentPerform(iterations: 10) { (index) in
            print("times:\(index)---- \(Thread.current)")
        }
        print("DispatchApply End")
    }

结果:
DispatchApply Begin
times:5---- <NSThread: 0x600001710640>{number = 11, name = (null)}
times:6---- <NSThread: 0x60000170c040>{number = 6, name = (null)}
times:1---- <NSThread: 0x600001793940>{number = 8, name = (null)}
times:0---- <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
times:4---- <NSThread: 0x600001751300>{number = 7, name = (null)}
times:7---- <NSThread: 0x600001727580>{number = 5, name = (null)}
times:2---- <NSThread: 0x600001750840>{number = 10, name = (null)}
times:8---- <NSThread: 0x600001793940>{number = 8, name = (null)}
times:9---- <_NSMainThread: 0x6000017040c0>{number = 1, name = main}
times:3---- <NSThread: 0x60000170cc40>{number = 9, name = (null)}
DispatchApply End

  • 所有任务都是并行执行的,循环执行并行队列中的任务时,会开辟新的线程,也有可能会在当前线程中执行一些任务。

2. 队列的挂起与唤醒

挂起是不会暂停正在执行的队列的哈,只能是挂起还没执行的队列。

private func dispatchSuspend() {
        let queue = DispatchQueue(label: "new thread")
        //        挂起
        queue.suspend()
        queue.async {
            print("The queue is suspended. Now it has completed.\n The queue is \"\(queue.label)\". ")
        }
        print("The thread will sleep for 3 seconds' time")
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.3) {
            //            唤醒,开始执行
            queue.resume()
        }
    }

结果:
The thread will sleep for 3 seconds’ time
The queue is suspended. Now it has completed.
The queue is “new thread”.

5. GCD队列组:DispatchGroup

有时候我们会有这样的需求:分别异步执行2个耗时任务,然后当2个耗时任务都执行完毕后再回到主线程执行任务。这时候我们可以用到 GCD 的队列组。

5.1 DispatchGroup.notify()

监听 group 中任务的完成状态,当所有的任务都执行完成后,追加任务到 group 中,并执行任务。

5.2 DispatchGroup.wait()

暂停当前线程(阻塞当前线程),等待指定的 group 中的任务执行完成后,才会往下继续执行。

5.3 DispatchGroup.enter()、DispatchGroup.leave()

  • enter() 标志着一个任务追加到 group,执行一次,相当于 group 中未执行完毕任务数+1
  • leave() 标志着一个任务离开了 group,执行一次,相当于 group 中未执行完毕任务数-1。
  • 当 group 中未执行完毕任务数为0的时候,才会使wait()解除阻塞,以及执行追加到notify()中的任务。

6. GCD信号量:DispatchSemaphore

GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。类似于过高速路收费站的栏杆。可以通过时,打开栏杆,不可以通过时,关闭栏杆。在 Dispatch Semaphore 中,使用计数来完成这个功能,计数为0时等待,不可通过。计数为1或大于1时,计数减1且不等待,可通过。

Dispatch Semaphore 三个函数:

  • DispatchSemaphore.init(value: 1):创建一个Semaphore并初始化信号的总量
  • DispatchSemaphore.signal():发送一个信号,让信号量加1
  • DispatchSemaphore.wait():可以使总信号量减1,当信号量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。

注意:信号量的使用前提是:想清楚你需要处理哪个线程等待(阻塞),又要哪个线程继续执行,然后使用信号量。

Dispatch Semaphore 在实际开发中主要用于:

  • 保持线程同步,将异步执行任务转换为同步执行任务
  • 保证线程安全,为线程加锁

六、GCD线程间的通信

在iOS开发过程中,一般在主线程里边进行UI刷新。我们通常把一些耗时的操作放在其他线程,比如说图片下载、文件上传等耗时操作。而当我们有时候在其他线程完成了耗时操作时,需要回到主线程,那么就用到了线程之间的通讯。

private func communicatioin() {
        // 获取全局并发队列
        let queue = DispatchQueue.global()
        // 获取主队列
        let mainQueue = DispatchQueue.main
        
        queue.async {
            // 异步追加任务
            for _ in 0..<2 {
                Thread.sleep(forTimeInterval: 2)
                print("1---\(Thread.current)")
            }
            // 回到主线程
            mainQueue.async {
                // 追加在主线程中执行的任务
                Thread.sleep(forTimeInterval: 2)
                print("2---\(Thread.current)")
            }
        }
    }

输出结果:
1—<NSThread: 0x600001754c40>{number = 8, name = (null)}
1—<NSThread: 0x600001754c40>{number = 8, name = (null)}
2—<_NSMainThread: 0x600001704a40>{number = 1, name = main}

说明:

  • 在其他线程中先执行任务,执行完了之后回到主线程执行主线程的相应操作。

文章出处:iOS多线程:『GCD』详尽总结

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值