Swift GCD 了解一下

1. GCD 簡(jiǎn)介

GCD是蘋(píng)果開(kāi)發(fā)的多線程編程的解決方案,通過(guò)簡(jiǎn)單的API就可以實(shí)現(xiàn)創(chuàng)建新線程去執(zhí)行我們需要執(zhí)行的任務(wù),不需要我們手動(dòng)地創(chuàng)建和管理線程,只需要?jiǎng)?chuàng)建隊(duì)列和相應(yīng)的函數(shù)配合使用就行。它的API包含在libdispatch庫(kù)中。

GCD全稱Grand Central Dispatch,是Apple提供的一套底層API,提供了一種新的方法來(lái)進(jìn)行并發(fā)程序編寫(xiě)。GCD有點(diǎn)像NSOperationQueue,但它比NSOpertionQueue更底層更高效,并且它不屬于Cocoa框架。GCD的API很大程度上基于block,當(dāng)然,GCD也可以脫離block來(lái)使用,比如使用傳統(tǒng)C機(jī)制提供函數(shù)指針和上下文指針。實(shí)踐證明,當(dāng)配合block使用時(shí),GCD非常簡(jiǎn)單易用且能發(fā)揮其最大能力。

2. GCD 的優(yōu)勢(shì)

  • GCD是蘋(píng)果公司為多核的并行運(yùn)算提出的解決方案
  • GCD會(huì)自動(dòng)利用更多的CPU內(nèi)核(比如雙核、四核)
  • GCD會(huì)自動(dòng)管理線程的生命周期(創(chuàng)建線程、調(diào)度任務(wù)、銷(xiāo)毀線程)
  • 程序員只需要告訴GCD想要執(zhí)行什么任務(wù),不需要編寫(xiě)任何線程管理代碼

3. GCD 任務(wù)和隊(duì)列

任務(wù)(Task): 就是執(zhí)行操作的意思,換句話說(shuō)就是你在線程中執(zhí)行的那段代碼。在 GCD 中是放在 block 中的。執(zhí)行任務(wù)有兩種方式:同步執(zhí)行(sync)異步執(zhí)行(async)。兩者的主要區(qū)別是:是否等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,以及是否具備開(kāi)啟新線程的能力。

  • 同步執(zhí)行(sync):
  • 同步添加任務(wù)到指定的隊(duì)列中,在添加的任務(wù)執(zhí)行結(jié)束之前,會(huì)一直等待,直到隊(duì)列里面的任務(wù)完成之后再繼續(xù)執(zhí)行。
  • 只能在當(dāng)前線程中執(zhí)行任務(wù),不具備開(kāi)啟新線程的能力。
  • 異步執(zhí)行(async):
  • 異步添加任務(wù)到指定的隊(duì)列中,它不會(huì)做任何等待,可以繼續(xù)執(zhí)行任務(wù)。
  • 可以在新的線程中執(zhí)行任務(wù),具備開(kāi)啟新線程的能力(但是并不一定開(kāi)啟新線程, 跟任務(wù)所指定的隊(duì)列類型有關(guān))。

隊(duì)列(Queue) 這里的隊(duì)列指執(zhí)行任務(wù)的等待隊(duì)列,即用來(lái)存放任務(wù)的隊(duì)列。隊(duì)列是一種特殊的線性表,采用 FIFO(先進(jìn)先出)的原則,即新任務(wù)總是被插入到隊(duì)列的末尾,而讀取任務(wù)的時(shí)候總是從隊(duì)列的頭部開(kāi)始讀取。每讀取一個(gè)任務(wù),則從隊(duì)列中釋放一個(gè)任務(wù)。

GCD中隊(duì)列的種類

  • 串行隊(duì)列(Serial Dispatch Queue): 每次只有一個(gè)任務(wù)被執(zhí)行。讓任務(wù)一個(gè)接著一個(gè)地執(zhí)行。(只開(kāi)啟一個(gè)線程,一個(gè)任務(wù)執(zhí)行完畢后,再執(zhí)行下一個(gè)任務(wù))
  • 并發(fā)隊(duì)列(Concurrent Dispatch Queue): 可以讓多個(gè)任務(wù)并發(fā)(同時(shí))執(zhí)行。(可以開(kāi)啟多個(gè)線程,并且同時(shí)執(zhí)行任務(wù)), 并發(fā)隊(duì)列的并發(fā)功能只有在異步(dispatch_async)函數(shù)下才有效

4.GCD 的簡(jiǎn)單使用

  • 創(chuàng)建一個(gè)隊(duì)列(串行隊(duì)列或并發(fā)隊(duì)列)
  • 將任務(wù)追加到任務(wù)的等待隊(duì)列中,然后系統(tǒng)就會(huì)根據(jù)任務(wù)類型執(zhí)行任務(wù)(同步執(zhí)行或異步執(zhí)行)

GCD創(chuàng)建隊(duì)列

  • 主隊(duì)列(串行隊(duì)列)

      let mainQueue = DispatchQueue.main
    
  • 全局并行隊(duì)列

      let globalQueue = DispatchQueue.global(qos: .default)
    
  • 創(chuàng)建串行隊(duì)列

      let serialQueue = DispatchQueue(label: "vip.mybadge")
    
  • 創(chuàng)建并行隊(duì)列

      let concurQueue = DispatchQueue(label: "vip.mybadge", attributes: .concurrent)
    

執(zhí)行任務(wù)

func task(i: Int) {
    print("\(i) thread = \(Thread.current)")
}
    
for i in 0..<100 {
    serialQueue.async {
        task(i: i)
    }
}
...
11 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
12 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
13 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
14 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
15 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
16 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
17 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
18 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
19 thread = <NSThread: 0x60000373fa00>{number = 5, name = (null)}
...

可以發(fā)現(xiàn)在串行隊(duì)列中, 等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,不具備開(kāi)啟新線程的能力

func task(i: Int) {
    print("\(i) thread = \(Thread.current)")
}
    
for i in 0..<100 {
    globalQueue.async {
        task(i: i)
    }
}
...
75 thread = <NSThread: 0x600002aef1c0>{number = 5, name = (null)}
76 thread = <NSThread: 0x600002aef1c0>{number = 5, name = (null)}
77 thread = <NSThread: 0x600002aef480>{number = 6, name = (null)}
78 thread = <NSThread: 0x600002aef1c0>{number = 5, name = (null)}
79 thread = <NSThread: 0x600002aef480>{number = 6, name = (null)}
80 thread = <NSThread: 0x600002af1280>{number = 8, name = (null)}
81 thread = <NSThread: 0x600002af16c0>{number = 9, name = (null)}
82 thread = <NSThread: 0x600002af1400>{number = 10, name = (null)}
83 thread = <NSThread: 0x600002af1340>{number = 11, name = (null)}
84 thread = <NSThread: 0x600002af1380>{number = 12, name = (null)}
...

可以發(fā)現(xiàn)在并行隊(duì)列中, 不等待隊(duì)列的任務(wù)執(zhí)行結(jié)束,具備開(kāi)啟新線程的能力

5. DispatchGroup

如果想等到所有的隊(duì)列的任務(wù)執(zhí)行完畢再進(jìn)行后序操作時(shí),可以使用DispatchGroup來(lái)完成。

let group = DispatchGroup()
for i in 0..<5 {
    print("任務(wù)\(i+1)下載中...")
    DispatchQueue.global().async(group: group) {
        Thread.sleep(forTimeInterval: 1)
        print("任務(wù)\(i+1)下載完成")
    }
}
group.notify(queue: DispatchQueue.main) {
    print("任務(wù)都下載完成...去更新UI")
}

執(zhí)行結(jié)果

任務(wù)1下載中...
任務(wù)2下載中...
任務(wù)3下載中...
任務(wù)4下載中...
任務(wù)5下載中...
任務(wù)1下載完成
任務(wù)3下載完成
任務(wù)5下載完成
任務(wù)4下載完成
任務(wù)2下載完成
任務(wù)都下載完成...去更新UI

6. DispatchWorkItem

Swift3新增的類,可以通過(guò)此類設(shè)置隊(duì)列執(zhí)行的任務(wù)。相當(dāng)于把原來(lái)GCD中閉包的代碼封裝到了這里,
看一個(gè)例子:

let workItem = DispatchWorkItem {
    for i in 0..<10 {
        print(i)
    }
}
DispatchQueue.global().async(execute: workItem)

看看他的初始化方法

init(qos: DispatchQoS = default, flags: DispatchWorkItemFlags = default,
block: @escaping () -> Void)

從初始化方法可以看出,DispatchWorkItem也可以設(shè)置優(yōu)先級(jí),另外還有個(gè)參數(shù)DispatchWorkItemFlags,來(lái)看看DispatchWorkItemFlags的內(nèi)部組成:

public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {
    // 相當(dāng)于之前的柵欄函數(shù)
    public static let barrier: DispatchWorkItemFlags 

    public static let detached: DispatchWorkItemFlags

    public static let assignCurrentContext: DispatchWorkItemFlags
    // 沒(méi)有優(yōu)先級(jí)
    public static let noQoS: DispatchWorkItemFlags 
    // 繼承Queue的優(yōu)先級(jí)
    public static let inheritQoS: DispatchWorkItemFlags      
    // 覆蓋Queue的優(yōu)先級(jí)
    public static let enforceQoS: DispatchWorkItemFlags
}

7. Dispatch barrier

可以理解為隔離,還是以文件讀寫(xiě)為例,在讀取文件時(shí),可以異步訪問(wèn),但是如果突然出現(xiàn)了異步寫(xiě)入操作,我們想要達(dá)到的效果是在進(jìn)行寫(xiě)入操作的時(shí)候,使讀取操作暫停,直到寫(xiě)入操作結(jié)束,再繼續(xù)進(jìn)行讀取操作,以保證讀取操作獲取的是文件的最新內(nèi)容。

先看看不使用barrier的例子

let concurQueue = DispatchQueue(label: "vip.mybadge", attributes: .concurrent)

struct File {
    var content = ""
}

var file = File()
file.content = "This is a file"

let writeFileWorkItem = DispatchWorkItem {
    file.content = "This file has been modified."
    Thread.sleep(forTimeInterval: 1)
    print("write file")
}

let readFileWorkItem = DispatchWorkItem {
    Thread.sleep(forTimeInterval: 1)
    print("file.content=\(file.content)")
}

for _ in 0..<3 {
    concurQueue.async(execute: readFileWorkItem)
}
concurQueue.async(execute: writeFileWorkItem)
for _ in 0..<3 {
    concurQueue.async(execute: readFileWorkItem)
}

輸出結(jié)果

file.content=This file has been modified.
write file
file.content=This file has been modified.
file.content=This file has been modified.
file.content=This file has been modified.
file.content=This file has been modified.
file.content=This file has been modified.

我們期望的結(jié)果是,在寫(xiě)文件之前,打印 "This is a file", 寫(xiě)文件之后打印的是"This file has been modified.", 上面結(jié)果顯然不是我們想要的。
看一下使用barrier的效果

let concurQueue = DispatchQueue(label: "vip.mybadge", attributes: .concurrent)

struct File {
    var content = ""
}

var file = File()
file.content = "This is a file"

let writeFileWorkItem = DispatchWorkItem(flags: .barrier) {
    file.content = "This file has been modified."
    Thread.sleep(forTimeInterval: 1)
    print("white file")
}
    
let readFileWorkItem = DispatchWorkItem {
    Thread.sleep(forTimeInterval: 1)
    print("file.content=\(file.content)")
}

for _ in 0..<3 {
    concurQueue.async(execute: readFileWorkItem)
}
concurQueue.async(execute: writeFileWorkItem)
for _ in 0..<3 {
    concurQueue.async(execute: readFileWorkItem)
}

輸出結(jié)果

file.content=This is a file.
file.content=This is a file.
file.content=This is a file.
write file
file.content=This file has been modified.
file.content=This file has been modified.
file.content=This file has been modified.

結(jié)果符合預(yù)期的想法,barrier主要用于讀寫(xiě)隔離,以保證寫(xiě)入的時(shí)候,不被讀取。

8. DispatchSemaphore

DispatchSemaphore中的信號(hào)量,可以解決資源搶占的問(wèn)題,支持信號(hào)的通知和等待.每當(dāng)發(fā)送一個(gè)信號(hào)通知,則信號(hào)量+1;每當(dāng)發(fā)送一個(gè)等待信號(hào)時(shí)信號(hào)量-1,如果信號(hào)量為0則信號(hào)會(huì)處于等待狀態(tài).直到信號(hào)量大于0開(kāi)始執(zhí)行.所以我們一般將DispatchSemaphore的value設(shè)置為1.

DispatchSemaphore 線程同步

線程同步: 可理解為線程A線程B一塊配合, A執(zhí)行到一定程度時(shí)要依靠B的某個(gè)結(jié)果, 于是停下來(lái), 示意B運(yùn)行; B依言執(zhí)行, 再將結(jié)果給A; A再繼續(xù)操作.

/// 信號(hào)量的線程同步.
func semaphoreSync() {
    var number = 0
    let semaphoreSignal = DispatchSemaphore(value: 0)
    let globalQueue = DispatchQueue.global()
    
    let workItem = DispatchWorkItem {
        Thread.sleep(forTimeInterval: 1)
        print("change number, thread=\(Thread.current)")
        number = 100
        semaphoreSignal.signal()
    }
    
    print("semaphore begin")
    print("number = \(number), thread=\(Thread.current)")
    globalQueue.async(execute: workItem)
    semaphoreSignal.wait()
    print("number = \(number)")
    print("semaphore end")
}

semaphoreSync()

輸出

semaphore begin
number = 0, thread=<NSThread: 0x6000007ca900>{number = 1, name = main}
change number, thread=<NSThread: 0x6000007e8180>{number = 5, name = (null)}
number = 100
semaphore end

semaphore end 是在執(zhí)行完 number = 100; 之后才打印的。而且輸出結(jié)果 number 為 100。

  • 這是因?yàn)楫惒綀?zhí)行不會(huì)做任何等待,可以繼續(xù)執(zhí)行任務(wù)。
  • 異步執(zhí)行將workItem追加到隊(duì)列之后,不做等待,接著去執(zhí)行semaphoreSignal.wait()方法。
  • 此時(shí) semaphore == 0,當(dāng)前線程進(jìn)入等待狀態(tài)。
  • 然后,workItem開(kāi)始執(zhí)行。workItem執(zhí)行到semaphoreSignal.signal()之后,
  • 信號(hào)量+1,此時(shí) semaphore == 1,semaphoreSignal.wait()方法使總信號(hào)量減1,正在被阻塞的線程(主線程)恢復(fù)繼續(xù)執(zhí)行。
  • 最后打印number = 100,semaphore---end,。

這樣就實(shí)現(xiàn)了線程同步,將異步執(zhí)行任務(wù)轉(zhuǎn)換為同步執(zhí)行任務(wù)。

Dispatch Semaphore 線程安全和線程同步(為線程加鎖)
  • 線程安全:如果你的代碼所在的進(jìn)程中有多個(gè)線程在同時(shí)運(yùn)行,而這些線程可能會(huì)同時(shí)運(yùn)行這段代碼。如果每次運(yùn)行結(jié)果和單線程運(yùn)行的結(jié)果是一樣的,而且其他的變量的值也和預(yù)期的是一樣的,就是線程安全的。否則就是 非線程安全的。
  • 若每個(gè)線程中對(duì)全局變量、靜態(tài)變量只有讀操作,而無(wú)寫(xiě)操作,一般來(lái)說(shuō),這個(gè)全局變量是線程安全的;若有多個(gè)線程同時(shí)執(zhí)行寫(xiě)操作(更改變量),一般都需要考慮線程同步,否則的話就可能影響線程安全。

下面,我們模擬火車(chē)票售賣(mài)的方式,實(shí)現(xiàn) NSThread 線程安全和解決線程同步問(wèn)題。

場(chǎng)景:總共有10張火車(chē)票,有兩個(gè)售賣(mài)火車(chē)票的窗口,一個(gè)是北京火車(chē)票售賣(mài)窗口,另一個(gè)是上?;疖?chē)票售賣(mài)窗口。兩個(gè)窗口同時(shí)售賣(mài)火車(chē)票,賣(mài)完為止。

非線程安全(不使用 semaphore)

先來(lái)看看不考慮線程安全的代碼

class SaleTicketNotSafe {
    private var ticketSurplusCount = 0
    private let semaphoreSignal = DispatchSemaphore(value: 1)
    private let serialQueue = DispatchQueue(label: "vip.mybadge.dispatch")
    private let serialQueue2 = DispatchQueue(label: "vip.mybadge.dispatch")
    
    init(ticketSurplusCount: Int) {
        self.ticketSurplusCount = ticketSurplusCount
    }
    
    func startSaleNotSave() {
        print("current thread=\(Thread.current)")
        serialQueue.async { [weak self] in
            self?.saleTicketNotSafe()
        }
        serialQueue2.async { [weak self] in
            self?.saleTicketNotSafe()
        }
    }
    
    private func saleTicketNotSafe() {
        while true {
            if ticketSurplusCount > 0 {
                ticketSurplusCount -= 1
                print("剩余票數(shù)\(ticketSurplusCount), 窗口:\(Thread.current)")
                Thread.sleep(forTimeInterval: 1)
            } else {
                print("所有票都售完了")
                break
            }
        }
    }
}

let saleTicket = SaleTicketNotSafe(ticketSurplusCount: 10)
saleTicket.startSaleNotSave()

輸出結(jié)果

開(kāi)始售票  thread=<NSThread: 0x600003802900>{number = 1, name = main}
剩余票數(shù)9, 窗口:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩余票數(shù)8, 窗口:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩余票數(shù)6, 窗口:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩余票數(shù)7, 窗口:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩余票數(shù)4, 窗口:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩余票數(shù)4, 窗口:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩余票數(shù)3, 窗口:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩余票數(shù)2, 窗口:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩余票數(shù)1, 窗口:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩余票數(shù)0, 窗口:<NSThread: 0x600003824c00>{number = 6, name = (null)}
所有票都售完了
所有票都售完了
線程安全 (使用 semaphore 加鎖)

線程安全的代碼

class SaleTicketSafe {
    private var ticketSurplusCount = 0
    private let semaphoreSignal = DispatchSemaphore(value: 1)
    private let serialQueue = DispatchQueue(label: "vip.mybadge.dispatch")
    private let serialQueue2 = DispatchQueue(label: "vip.mybadge.dispatch")
    init(ticketSurplusCount: Int) {
        self.ticketSurplusCount = ticketSurplusCount
    }
   
    func startSaleSave() {
        print("開(kāi)始售票 thread=\(Thread.current)")
        serialQueue.async { [weak self] in
            self?.saleTicketSafe()
        }
        serialQueue2.async { [weak self] in
            self?.saleTicketSafe()
        }
    }
    private func saleTicketSafe() {
        while true {
            semaphoreSignal.wait()
            if ticketSurplusCount > 0 {
                ticketSurplusCount -= 1
                print("剩余票數(shù)\(ticketSurplusCount), 窗口:\(Thread.current)")
                Thread.sleep(forTimeInterval: 1)
            } else {
                semaphoreSignal.signal()
                print("所有票都售完了")
                break
            }
            semaphoreSignal.signal()
        }
    }
}

let saleTicket = SaleTicketSafe(ticketSurplusCount: 10)
saleTicket.startSaleSave()

輸出結(jié)果

開(kāi)始售票 thread=<NSThread: 0x600001ac6900>{number = 1, name = main}
剩余票數(shù)9, 窗口:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩余票數(shù)8, 窗口:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩余票數(shù)7, 窗口:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩余票數(shù)6, 窗口:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩余票數(shù)5, 窗口:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩余票數(shù)4, 窗口:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩余票數(shù)3, 窗口:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩余票數(shù)2, 窗口:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩余票數(shù)1, 窗口:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩余票數(shù)0, 窗口:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
所有票都售完了
所有票都售完了

可以看出,在考慮了線程安全的情況下,使用 DispatchSemaphore 機(jī)制之后,得到的票數(shù)是正確的,沒(méi)有出現(xiàn)混亂的情況。我們也就解決了線程安全與線程同步的問(wèn)題。

以上

以上代碼可以直接在Playground中運(yùn)行

為總結(jié)學(xué)習(xí)而寫(xiě),若有錯(cuò)誤,歡迎指正。

參考 Swift 3必看:從使用場(chǎng)景了解GCD新API

參考 Swift多線程編程總結(jié)

參考 線程安全: 互斥鎖和自旋鎖(10種)

參考 iOS多線程:『GCD』詳盡總結(jié)

參考 菜鳥(niǎo)不要怕,看一眼,你就會(huì)用GCD,帶你裝逼帶你飛

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容