走進(jìn)Swift 3 中的 GCD

在 iOS 當(dāng)中,蘋果提供了兩種方式進(jìn)行多任務(wù)編程:Grand Central Dispatch (GCD) 和 NSOperationQueue。當(dāng)我們需要把任務(wù)分配到不同的線程中,或者是非主隊列的其它隊列中時,這兩種方法都可以很好地滿足需求。選擇哪一種方法是很主觀的行為,但是該文章關(guān)注前一種,即 GCD。不管使用哪一種方法,有一條規(guī)則必須要牢記: 任何操作都不能堵塞主線程,必須使其用于界面響應(yīng)以及用戶交互。所有的耗時操作或者對 CPU 需求大的任務(wù)都要在并發(fā)或者后臺隊列中執(zhí)行。對于新手來說,理解和實踐可能都會比較難。

我們需要先了解一些更具體的概念。首先,GCD 中的核心詞是 dispatch queue。一個隊列實際上就是一系列的代碼塊,這些代碼可以在主線程或后臺線程中以同步或者異步的方式執(zhí)行。一旦隊列創(chuàng)建完成,操作系統(tǒng)就接管了這個隊列,并將其分配到任意一個核心中進(jìn)行處理。不管有多少個隊列,它們都能被系統(tǒng)正確地管理,這些都不需要開發(fā)者進(jìn)行手動管理。隊列遵循 FIFO 模式(先進(jìn)先出),這意味著先進(jìn)隊列的任務(wù)會先被執(zhí)行(想像在柜臺前排隊的隊伍,排在第一個的會首先被服務(wù),排在最后的就會最后被服務(wù))。我們會在后面的第一個例子中更清楚地理解這個概念。

接下來,另一個重要的概念就是 WorkItem(任務(wù)項)。一個任務(wù)項就是一個代碼塊,它可以隨同隊列的創(chuàng)建一起被創(chuàng)建,也可以被封裝起來,然后在之后的代碼中進(jìn)行復(fù)用。正如你所想,任務(wù)項的代碼就是 dispatch queue 將會執(zhí)行的代碼。隊列中的任務(wù)項也是遵循 FIFO 模式。這些執(zhí)行可以是同步的,也可以是異步的。對于同步的情況下,應(yīng)用會一直堵塞當(dāng)前線程,直到這段代碼執(zhí)行完成。而當(dāng)異步執(zhí)行的時候,應(yīng)用先執(zhí)行任務(wù)項,不等待執(zhí)行結(jié)束,立即返回。我們會在后面的實例里看到它們的區(qū)別。

1. 認(rèn)識 Dispatch Queue

在 Swift 3 當(dāng)中,創(chuàng)建一個 dispatch queue 的最簡單方式如下:

let queue = DispatchQueue(label: "com.appcoda.myqueue")

你唯一要做的事就是為你的隊列提供一個獨一無二的標(biāo)簽(label)。使用一個反向的 DNS 符號(”com.appcoda.myqueue”)就很好,因為用它很容易創(chuàng)造一個獨一無二的標(biāo)簽,甚至連蘋果公司都是這樣建議的。盡管如此,這并不是強(qiáng)制性的,你可以使用你喜歡的任何字符串,只要這個字符串是唯一的。除此之外,上面的構(gòu)造方法并不是創(chuàng)建隊列的唯一方式。在初始化隊列的時候可以提供更多的參數(shù),我們會在后面的篇幅中談?wù)摰剿?br> 一旦隊列被創(chuàng)建后,我們就可以使用它來執(zhí)行代碼了,可以使用 sync 方法來進(jìn)行同步執(zhí)行,或者使用 async 方法來進(jìn)行異步執(zhí)行。因為我們剛開始,所以先使用代碼塊(一個閉包)來作為被執(zhí)行的代碼。在后面的篇幅中,我們會初始化并使用 dispatch 任務(wù)項(DispatchWorkItem)來取代代碼塊(需要注意的是,對于隊列來說代碼塊也算是一個任務(wù)項)。我們先從同步執(zhí)行開始,下面要做的就是打印出數(shù)字 0~9 :



程序的運行會在隊列的 block 中止,并且直到隊列的任務(wù)結(jié)束前,它都不會執(zhí)行主線程,也不會打印數(shù)字 100 ~ 109。程序會有這樣的行為,是因為我們使用了同步執(zhí)行。你也可以在控制臺中看到輸出結(jié)果

但是如果我們使用 async 方法運行代碼塊會發(fā)生什么事呢?在這種情況下,程序不需要等待隊列任務(wù)完成才往下執(zhí)行,它會立馬返回主線程,然后第二個 for 循環(huán)會與隊列里的循環(huán)同時運行。在我們看到會發(fā)生什么事之前,將隊列的執(zhí)行改用 async 方法



盡管上面的示例很簡單,但已經(jīng)清楚地展示了一個程序在同步隊列與異步隊列中行為的差異。我們將在接下來的示例中繼續(xù)使用這種彩色的控制臺輸出,請記住,特定顏色代碼特定隊列的運行結(jié)果,不同的顏色代表不同的隊列。

2. Quality Of Service(QoS)和優(yōu)先級

在使用 GCD 與 dispatch queue 時,我們經(jīng)常需要告訴系統(tǒng),應(yīng)用程序中的哪些任務(wù)比較重要,需要更高的優(yōu)先級去執(zhí)行。當(dāng)然,由于主隊列總是用來處理 UI 以及界面的響應(yīng),所以在主線程執(zhí)行的任務(wù)永遠(yuǎn)都有最高的優(yōu)先級。不管在哪種情況下,只要告訴系統(tǒng)必要的信息,iOS 就會根據(jù)你的需求安排好隊列的優(yōu)先級以及它們所需要的資源(比如說所需的 CPU 執(zhí)行時間)。雖然所有的任務(wù)最終都會完成,但是,重要的區(qū)別在于哪些任務(wù)更快完成,哪些任務(wù)完成得更晚。
用于指定任務(wù)重要程度以及優(yōu)先級的信息,在 GCD 中被稱為 Quality of Service(QoS)。事實上,QoS 是有幾個特定值的枚舉類型,我們可以根據(jù)需要的優(yōu)先級,使用合適的 QoS 值來初始化隊列。如果沒有指定 QoS,則隊列會使用默認(rèn)優(yōu)先級進(jìn)行初始化。要詳細(xì)了解 QoS 可用的值,可以參考這個文檔,請確保你仔細(xì)看過這個文檔。下面的列表總結(jié)了 Qos 可用的值,它們也被稱為 QoS classes。第一個 class 代碼了最高的優(yōu)先級,最后一個代表了最低的優(yōu)先級:
userInteractive
userInitiated
default
utility
background
unspecified

let queue1 = DispatchQueue(label: "com.appcoda.queue1", qos: DispatchQoS.userInitiated)
let queue2 = DispatchQueue(label: "com.appcoda.queue2", qos: DispatchQoS.userInitiated)

從上面的截圖當(dāng)中可以輕易看出這兩個任務(wù)被“均勻”地執(zhí)行,而這也是我們預(yù)期的結(jié)果?,F(xiàn)在讓我們把 queue2 的 QoS class 設(shè)置為 utility(低優(yōu)先級),如下所示
現(xiàn)在看看會發(fā)生什么:



毫無疑問地,第一個 dispatch queue(queue1)比第二個執(zhí)行得更快,因為它的優(yōu)先級比較高。即使 queue2 在第一個隊列執(zhí)行的時候也獲得了執(zhí)行的機(jī)會,但由于第一個隊列的優(yōu)先級比較高,所以系統(tǒng)把多數(shù)的資源都分配給了它,只有當(dāng)它結(jié)束后,系統(tǒng)才會去關(guān)心第二個隊列。
現(xiàn)在讓我們再做另外一個試驗,這次將第一個 queue 的 QoS class 設(shè)置為 background:

3. 并行隊列

在上面的初始化當(dāng)中,有一個新的參數(shù):attributes。當(dāng)這個參數(shù)被指定為 concurrent 時,該特定隊列中的所有任務(wù)都會被同時執(zhí)行。如果沒有指定這個參數(shù),則隊列會被設(shè)置為串行隊列。事實上,QoS 參數(shù)也不是必須的,在上面的初始化中,即使我們將這些參數(shù)去掉也不會有任何問題。
現(xiàn)在重新運行代碼,可以看到任務(wù)都被并行地執(zhí)行了:


注意,改變 QoS class 也會影響程序的運行。但是,只要在初始化隊列的時候指定了 concurrent,這些任務(wù)就會以并行的方式運行,并且它們各自都會擁有運行時間。
這個 attributes 參數(shù)也可以接受另一個名為 initiallyInactive 的值。如果使用這個值,任務(wù)不會被自動執(zhí)行,而是需要開發(fā)者手動去觸發(fā)。我們接下來會進(jìn)行說明,但是在這之前,需要對代碼進(jìn)行一些改動。首先,聲明一個名為 inactiveQueue 的成員屬性,如下所示

if let queue = inactiveQueue {
    queue.activate()
}

現(xiàn)在的問題是,我們?nèi)绾卧谥付?initiallyInactive 的同時將隊列指定為并行隊列?其實很簡單,我們可以將兩個值放入一個數(shù)組當(dāng)中,作為 attributes 的參數(shù),替代原本指定的單一數(shù)值:

4. 延遲執(zhí)行

有時候,程序需要對代碼塊里面的任務(wù)項進(jìn)行延時操作。GCD 允許開發(fā)者通過調(diào)用一個方法來指定某個任務(wù)在延遲特定的時間后再執(zhí)行。

5. 訪問主隊列和全局隊列

在前面的所有例子當(dāng)中,我們都手動創(chuàng)建了要使用的 dispatch queue。實際上,我們并不總是需要自己手動創(chuàng)建,特別是當(dāng)我們不需要改變隊列的優(yōu)先級的時候。就像我在文章一開頭講過的,操作系統(tǒng)會創(chuàng)建一個后臺隊列的集合,也被稱為全局隊列(global queue)。你可以像使用自己創(chuàng)建的隊列一樣來使用它們,只是要注意不能濫用。
訪問全局隊列十分簡單:

let globalQueue = DispatchQueue.global()

可以像我們之前使用過的隊列一樣來使用它:


當(dāng)使用全局隊列的時候,并沒有太多的屬性可供我們進(jìn)行修改。但是,你仍然可以指定你想要使用隊列的 Quality of Service:

let globalQueue = DispatchQueue.global(qos: .userInitiated)

如果沒有指定 QoS class(就像本節(jié)的第一個示例),就會默認(rèn)以 default 作為默認(rèn)值。
無論你使不使用全局隊列,你都不可避免地要經(jīng)常訪問主隊列,大多數(shù)情況下是作為更新 UI 而使用。在其它隊列中訪問主隊列的方法也非常簡單,就如下面的代碼片段所示,并且需要在調(diào)用的同時指定同步還是異步執(zhí)行:

DispatchQueue.main.async {
    // Do something
}

事實上,你可以輸入 DispatchQueue.main. 來查看主隊列的所有可用選項,Xcode 會通過自動補全來顯示主隊列所有可用的方法,不過上面代碼展示的就是我們絕大多數(shù)時間會用到的(事實上,這個方法是通用的,對于所有隊列,都可以通過輸入 . 之后讓 Xcode 來進(jìn)行自動補全)。就像上一節(jié)所做的一樣,你也可以為代碼的執(zhí)行增加延時。
現(xiàn)在讓我們來看一個真實的案例,演示如何通過主隊列來更新 UI。在初始工程的 Main.storyboard 文件中有一個 ViewController 場景(sence),這個 ViewController 場景包含了一個 imageView,并且這個 imageView 已經(jīng)通過 IBOutlet 連接到對應(yīng)的 ViewController 類文件中。在這里,我們通過 fetchImage() 方法(目前是空的)來下載一個 Appcoda 的 logo 并將其展示到 imageView 當(dāng)中。下面的代碼完成了上述動作(我不會在這里針對 URLSession 做相關(guān)的討論,以及介紹它如何使用):

func fetchImage() {
    let imageURL: URL = URL(string: "http://www.appcoda.com/wp-content/uploads/2015/12/blog-logo-dark-400.png")!
    (URLSession(configuration: URLSessionConfiguration.default)).dataTask(with: imageURL, completionHandler: { (imageData, response, error) in
        if let data = imageData {
            print("Did download image data")
            self.imageView.image = UIImage(data: data)
        }
    }).resume()
}

意,我們并沒有在主隊列更新 UI 界面,而是試圖在 dataTask(...) 方法的 completion handler 里運行的后臺線程來更新界面。編譯、運行程序,看看會發(fā)生什么(不要忘記調(diào)用 fetchImage() 方法):


即使我們得到了圖片下載完成的信息,但是沒有看到圖片被顯示到 imageView 上面,這是因為 UI 并沒有更新。大多數(shù)情況下,這個圖片會在信息出現(xiàn)的一小會后顯示出來(但是如果其他任務(wù)也在應(yīng)用程序中執(zhí)行,上述情況不保證會發(fā)生),不僅如此,你還會在控制臺看到關(guān)于在后臺線程更新 UI 的一大串出錯信息。
現(xiàn)在,讓我們改正這段有問題的行為,使用主隊列來更新用戶界面。在編輯上述方法的時候,只需要改動底下所示部分,并注意我們是如何使用主隊列的:

if let data = imageData {
    print("Did download image data")
    DispatchQueue.main.async {
        self.imageView.image = UIImage(data: data)
    }
}

再次運行程序,會看到圖片在下載完成后被正確地顯示出來。主隊列確實被調(diào)用并更新了 UI。

6. 使用 DispatchWorkItem 對象

DispatchWorkItem 是一個代碼塊,它可以在任意一個隊列上被調(diào)用,因此它里面的代碼可以在后臺運行,也可以在主線程運行。它的使用真的很簡單,就是一堆可以直接調(diào)用的代碼,而不用像之前一樣每次都寫一個代碼塊。
下面展示了使用任務(wù)項最簡單的方法:

let workItem = DispatchWorkItem {
    // Do something
}

現(xiàn)在讓我們通過一個小例子來看看 DispatchWorkItem 如何使用。前往 useWorkItem() 方法,并添加如下代碼:

func useWorkItem() {
    var value = 10
    let workItem = DispatchWorkItem {
        value += 5
    }
}

這個任務(wù)項的目的是將變量 value 的值增加 5。我們使用任務(wù)項對象去調(diào)用 perform() 方法,如下所示:

workItem.perform()

這行代碼會在主線程上面調(diào)用任務(wù)項,但是你也可以使用其它隊列來執(zhí)行它。參考下面的示例:

let queue = DispatchQueue.global()
queue.async {
    workItem.perform()
}

這段代碼也可以正常運行。但是,有一個更快地方法可以達(dá)到同樣的效果。DispatchQueue 類為此目的提供了一個便利的方法:

queue.async(execute: workItem)

當(dāng)一個任務(wù)項被調(diào)用后,你可以通知主隊列(或者任何其它你想要的隊列),如下所示:

workItem.notify(queue: DispatchQueue.main) {
    print("value = ", value)
}

上面的代碼會在控制臺打印出 value 變量的值,并且它是在任務(wù)項被執(zhí)行的時候打印的?,F(xiàn)在將所有代碼放到一起,userWorkItem() 方法內(nèi)的代碼如下所示

func useWorkItem() {
    var value = 10
    let workItem = DispatchWorkItem {
        value += 5
    }
    workItem.perform()
    let queue = DispatchQueue.global(qos: .utility)
    queue.async(execute: workItem)
    workItem.notify(queue: DispatchQueue.main) {
        print("value = ", value)
    }
}
7. 創(chuàng)建線程群組
func group() {
        // 獲得全局隊列
        let globalQueue = DispatchQueue.global()

        // 創(chuàng)建一個隊列組
        let group = DispatchGroup()

        globalQueue.async(group: group, execute: {
            print("任務(wù)一 \(Thread.current)")
        })
        globalQueue.async(group: group, execute: {
            print("任務(wù)二 \(Thread.current)")
        })

        // group內(nèi)的任務(wù)完成后,執(zhí)行此方法
        group.notify(queue: globalQueue, execute: {
            print("終極任務(wù) \(Thread.current)")
        })

        globalQueue.async(group: group, execute: {
            print("任務(wù)三 \(Thread.current)")
        })

        globalQueue.async(group: group, execute: {
            print("任務(wù)四 \(Thread.current)")
        })
    }

開啟多條線程,去執(zhí)行群組中的任務(wù),當(dāng)群組內(nèi)的四個任務(wù)執(zhí)行完畢后,再去執(zhí)行notify里面的任務(wù)

8. GCD 信號量控制并發(fā)

當(dāng)我們在處理一系列線程的時候,當(dāng)數(shù)量達(dá)到一定量,在以前我們可能會選擇使用NSOperationQueue來處理并發(fā)控制,但如何在GCD中快速的控制并發(fā)呢?答案就是dispatch_semaphore。

信號量是一個整形值并且具有一個初始計數(shù)值,并且支持兩個操作:信號通知和等待。當(dāng)一個信號量被信號通知,其計數(shù)會被增加。當(dāng)一個線程在一個信號量上等待時,線程會被阻塞(如果有必要的話),直至計數(shù)器大于零,然后線程會減少這個計數(shù)。

在GCD中有三個函數(shù)是semaphore的操作,分別是:
1、dispatch_semaphore_create   創(chuàng)建一個semaphore
2、dispatch_semaphore_signal   發(fā)送一個信號
3、dispatch_semaphore_wait    等待信號
下面我們逐一介紹三個函數(shù):
(1) dispatch_semaphore_create
  dispatch_semaphore_t dispatch_semaphore_create(long value);
傳入的參數(shù)為long,輸出一個dispatch_semaphore_t類型且值為value的信號量。值得注意的是,這里的傳入的參數(shù)value必須大于或等于0,否則dispatch_semaphore_create會返回NULL。
(2) dispatch_semaphore_signal
 long dispatch_semaphore_signal(dispatch_semaphore_t dsema)這個函數(shù)會使傳入的信號量dsema的值加1
(3) dispatch_semaphore_wait
 long dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout)
這個函數(shù)會使傳入的信號量dsema的值減1。這個函數(shù)的作用是這樣的,如果dsema信號量的值大于0,該函數(shù)所處線程就繼續(xù)執(zhí)行下面的語句,并且將信號量的值減1;如果desema的值為0,那么這個函數(shù)就阻塞當(dāng)前線程等待timeout(注意timeout的類型為dispatch_time_t,不能直接傳入整形或float型數(shù)),如果等待的期間desema的值被dispatch_semaphore_signal函數(shù)加1了,且該函數(shù)(即dispatch_semaphore_wait)所處線程獲得了信號量,那么就繼續(xù)向下執(zhí)行并將信號量減1。如果等待期間沒有獲取到信號量或者信號量的值一直為0,那么等到timeout時,其所處線程自動執(zhí)行其后語句
(4)dispatch_semaphore_signal的返回值為long類型,當(dāng)返回值為0時表示當(dāng)前并沒有線程等待其處理的信號量,其處理的信號量的值加1即可。當(dāng)返回值不為0時,表示其當(dāng)前有(一個或多個)線程等待其處理的信號量,并且該函數(shù)喚醒了一個等待的線程(當(dāng)線程有優(yōu)先級時,喚醒優(yōu)先級最高的線程;否則隨機(jī)喚醒)。
  dispatch_semaphore_wait的返回值也為long型。當(dāng)其返回0時表示在timeout之前,該函數(shù)所處的線程被成功喚醒。當(dāng)其返回不為0時,表示timeout發(fā)生。
(5)停車場剩余4個車位,那么即使同時來了四輛車也能停的下。如果此時來了五輛車,那么就有一輛需要等待。信號量的值就相當(dāng)于剩余車位的數(shù)目,dispatch_semaphore_wait函數(shù)就相當(dāng)于來了一輛車,dispatch_semaphore_signal就相當(dāng)于走了一輛車。停車位的剩余數(shù)目在初始化的時候就已經(jīng)指明了(dispatch_semaphore_create(long value)),調(diào)用一次dispatch_semaphore_signal,剩余的車位就增加一個;調(diào)用一次dispatch_semaphore_wait剩余車位就減少一個;當(dāng)剩余車位為0時,再來車(即調(diào)用dispatch_semaphore_wait)就只能等待。有可能同時有幾輛車等待一個停車位。有些車主沒有耐心,給自己設(shè)定了一段等待時間,這段時間內(nèi)等不到停車位就走了,如果等到了就開進(jìn)去停車。而有些車主就像把車停在這,所以就一直等下去。

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

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

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