多線程之GCD與NSOperation

開始之前

首先要解決一個大家對多線程的理解上可能存在的誤區(qū):新開一個線程,能提高速度,避免阻塞主線程。
這句話看著好像是對著呢,但是仔細(xì)想想這句話是不那么準(zhǔn)確的。

舉個例子:一個主任務(wù)需要十個子任務(wù)按順序執(zhí)行來完成。現(xiàn)在有兩種方式完成這個任務(wù):
1.建十個線程,把每個子任務(wù)放在對應(yīng)的線程中執(zhí)行。執(zhí)行完一個線程中的任務(wù)就切換到另一個線程。
2.把十個任務(wù)放在一個線程里,按順序執(zhí)行。

操作系統(tǒng)的基礎(chǔ)知識告訴我們,線程,是執(zhí)行程序最基本的單元,它有自己棧和寄存器。說得再具體一些,線程就是“一個CPU執(zhí)行的一條無分叉的命令列”。

對于第一種方法,在十個線程之間來回切換,就意味著有十組棧和寄存器中的值需要不斷地被備份、替換。 而對于對于第二種方法,只有一組寄存器和棧存在,顯然效率完勝前者。

并發(fā)與并行的區(qū)別

并發(fā)指的是一種現(xiàn)象,一種經(jīng)常出現(xiàn),無可避免的現(xiàn)象。它描述的是“多個任務(wù)同時發(fā)生,需要被處理”這一現(xiàn)象。它的側(cè)重點在于“發(fā)生”。
比如有很多人排隊等待檢票,這一現(xiàn)象就可以理解為并發(fā)。

并行指的是一種技術(shù),一個同時處理多個任務(wù)的技術(shù)。它描述了一種能夠同時處理多個任務(wù)的能力,側(cè)重點在于“運行”。
比如景點開放了多個檢票窗口,同一時間內(nèi)能服務(wù)多個游客。這種情況可以理解為并行。

并行的反義詞就是串行,表示任務(wù)必須按順序來,一個一個執(zhí)行,前一個執(zhí)行完了才能執(zhí)行后一個。

我們經(jīng)常提到的“多線程”,正是采用了并行技術(shù),從而提高了執(zhí)行效率。因為有多個線程,所以計算機(jī)的多個CPU可以同時工作,同時處理不同線程內(nèi)的指令。

并發(fā)是一種現(xiàn)象,面對這一現(xiàn)象,我們首先創(chuàng)建多個線程,真正加快程序運行速度的,是并行技術(shù)。也就是讓多個CPU同時工作。而多線程,是為了讓多個CPU同時工作成為可能。

同步與異步

同步方法就是我們平時調(diào)用的哪些方法。比如在第一行調(diào)用a方法,那么程序運行到第二行的時候,a方法肯定是執(zhí)行完了。

所謂的異步,就是允許在執(zhí)行某一個任務(wù)時,函數(shù)立刻返回,但是真正要執(zhí)行的任務(wù)稍后完成。

比如我們在點擊保存按鈕之后,要先把數(shù)據(jù)寫到內(nèi)存,然后更新UI。同步方法就是等到數(shù)據(jù)保存完再更新UI,而異步則是立刻從保存數(shù)據(jù)的方法返回并向后執(zhí)行代碼,同時真正用來保存數(shù)據(jù)的指令將在稍后執(zhí)行。

區(qū)別和聯(lián)系

串行/并行針對的是隊列,而同步/異步,針對的則是線程。最大的區(qū)別在于,同步線程要阻塞當(dāng)前線程,必須要等待同步線程中的任務(wù)執(zhí)行完,返回以后,才能繼續(xù)執(zhí)行下一任務(wù);而異步線程則是不用等待。

假設(shè)現(xiàn)在有三個任務(wù)需要處理。假設(shè)單個CPU處理它們分別需要3、1、1秒。

并行/串行討論的是處理這三個任務(wù)的速度問題。如果三個CPU并行處理,那么一共只需要3秒。相比于串行處理,節(jié)約了兩秒。

同步/異步描述的是任務(wù)之間先后順序問題。假設(shè)需要三秒的那個是保存數(shù)據(jù)的任務(wù),而另外兩個是UI相關(guān)的任務(wù)。那么通過異步執(zhí)行第一個任務(wù),我們省去了三秒鐘的卡頓時間。

對于同步執(zhí)行的三個任務(wù)來說,系統(tǒng)傾向于在同一個線程里執(zhí)行它們。因為即使開了三個線程,也得等他們分別在各自的線程中完成。并不能減少總的處理時間,反而徒增了線程切換所耗費的時間。

對于異步執(zhí)行的三個任務(wù)來說,系統(tǒng)傾向于在三個新的線程里執(zhí)行他們。因為這樣可以最大程度的利用CPU性能,提升程序運行效率。

總結(jié)

在需要同時處理寫入寫出操作和UI操作的情況下,真正起作用的是異步,而不是多線程??梢圆挥枚嗑€程,但不能不用異步。

GCD

GCD以block(Swift中是閉包,為了方便,下面都以block表示)為基本單位,一個block中的代碼可以為一個任務(wù)。下文中提到任務(wù),可以理解為執(zhí)行某個block。

同時,GCD中有兩大最重要的概念,分別是“隊列”和“執(zhí)行方式”。

使用block的過程,概括來說就是把block放進(jìn)合適的隊列,并選擇合適的執(zhí)行方式去執(zhí)行block的過程。

三種隊列:
串行隊列(先進(jìn)入隊列的任務(wù)先出隊列,每次只執(zhí)行一個任務(wù))
并發(fā)隊列(依然是“先入先出”,不過可以形成多個任務(wù)并發(fā))
主隊列(這是一個特殊的串行隊列,而且隊列中的任務(wù)一定會在主線程中執(zhí)行)

兩種執(zhí)行方式:
同步執(zhí)行
異步執(zhí)行

關(guān)于同步/異步、串行/并行和線程的關(guān)系,下面通過一個表格來總結(jié):

同步 異步
主隊列 在主線程中執(zhí)行 在主線程中執(zhí)行
串行隊列 在當(dāng)前線程中執(zhí)行 新建線程執(zhí)行
并發(fā)隊列 在當(dāng)前線程中執(zhí)行 新建線程執(zhí)行

可以看到,同步方法不一定在本線程,因為加入到主隊列的就會在主線程內(nèi)執(zhí)行;異步方法方法也不一定新開線程,也是因為主隊列的特殊情況。

在我們的實際開發(fā)過程中,我們要更多考慮的是怎么準(zhǔn)確的使用好串行/并行、同步/異步,而不是僅僅只考慮是否新開線程這個問題。

當(dāng)然,了解任務(wù)運行在那個線程中也是為了更加深入的理解整個程序的運行情況,尤其是接下來要討論的死鎖問題。

GCD的死鎖問題

在使用GCD的過程中,如果向當(dāng)前串行隊列中同步派發(fā)一個任務(wù),就會導(dǎo)致死鎖。
這句話有點繞,先舉個例子看看:

override func viewDidLoad() {
        super.viewDidLoad()

        let queue = DispatchQueue.main
        
        queue.sync {
            print("啊哈哈")
        }
}

這段代碼就會導(dǎo)致死鎖,因為我們目前在主隊列中,又將要同步地添加一個block到主隊列中。

先分析一波

我們知道.sync表示同步的執(zhí)行任務(wù),也就是說執(zhí)行.sync后,當(dāng)前線程會阻塞。而.sync中的block如果要在當(dāng)前線程中執(zhí)行,就得等待當(dāng)前線程執(zhí)行完成。

在上面這個例子中,主線程在執(zhí)行.sync,隨后主隊列中新增一個任務(wù)block。因為主隊列是串行隊列,所以block要等.sync執(zhí)行完才能執(zhí)行,但是.sync是同步派發(fā),要等block執(zhí)行完才算是結(jié)束。在主隊列中的兩個任務(wù)互相等待,導(dǎo)致了死鎖。

解決方案

其實在通常情況下我們不必要用.sync,因為.async能夠更好的利用CPU,提升程序運行速度。只有當(dāng)我們需要保證隊列中的任務(wù)必須順序執(zhí)行時,才考慮.sync。在使用.sync的時候應(yīng)該分析當(dāng)前處于哪個隊列,以及任務(wù)會提交到哪個隊列。

DispatchGroup

在平時的開發(fā)過程中,可能會有這樣的需求:我需要在完成一些任務(wù)之后緊接著去執(zhí)行另外一個任務(wù),這里,我們就可以使用GCD任務(wù)組解決類似需求。
在單個串行隊列中,這個需求不是問題,因為只要把回調(diào)block添加到隊列末尾即可。
但是對于并行隊列,以及多個串行、并行隊列混合的情況,就需要使用DispatchGroup了。

  override func viewDidLoad() {
        super.viewDidLoad()

        let concurrentQueue = DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil)
        let serialQueue = DispatchQueue(label: "serialQueue")
        
        let group = DispatchGroup()
        
        for i in 0...3 {
            concurrentQueue.async(group: group, qos: .default, flags: []) {
                print("concurrentQueue\(i)")
            }
        }
        
        for i in 0...3 {
            serialQueue.async(group: group, qos: .default, flags: []) {
                print("serialQueue\(i)")
            }
        }
        
        //執(zhí)行完上面的兩個耗時操作, 回到主隊列中執(zhí)行下一步的任務(wù)
        group.notify(queue: DispatchQueue.main) {
            print("回到主隊列執(zhí)行一些操作")
        }
  }
  輸出:
  concurrentQueue1
  serialQueue0
  concurrentQueue2
  concurrentQueue0
  concurrentQueue3
  serialQueue1
  serialQueue2
  serialQueue3
  回到主隊列執(zhí)行一些操作

首先創(chuàng)建一個并發(fā)隊列和串行隊列,然后通過DispatchGroup()方法生成一個組。

接下來,在兩個不同的隊列里面分別加入不同的任務(wù),并放入到group中去。

最后調(diào)用group.notify方法。這個方法表示把第二個參數(shù) block 傳入第一個參數(shù)隊列中去。而且可以保證第二個參數(shù) block 執(zhí)行時,group中的所有任務(wù)已經(jīng)全部完成。

.asyncAfter方法

通過 GCD 還可以進(jìn)行簡單的定時操作,比如在 1 秒后執(zhí)行某個 block 。代碼如下:

DispatchQueue.main.asyncAfter(deadline:DispatchTime.now() + 1 ) {
    print("我是在一秒后執(zhí)行的")
}

.asyncAfter方法的調(diào)用者表示要執(zhí)行的任務(wù)提交到哪個隊列,后面有兩個參數(shù)。第一個表示時間,也就是從現(xiàn)在起往后一秒鐘。第二個參數(shù)分別表示要提交的任務(wù)。

需要注意的是.asyncAfter僅表示在指定時間后提交任務(wù),而非執(zhí)行任務(wù)。如果任務(wù)提交到主隊列,它將在main runloop中執(zhí)行,對于每隔1/60秒執(zhí)行一次的RunLoop,任務(wù)最多有可能在1+1/60秒后執(zhí)行。

Operation

OperationOperationQueue主要涉及這幾個方面:

  • OperationOperationQueue用法介紹
  • Operation的暫停、恢復(fù)和取消
  • 通過 KVO 對 Operation的狀態(tài)進(jìn)行檢測
  • 多個 Operation 的之間的依賴關(guān)系

從簡單意義上來說,Operation是對 GCD 中的 block 進(jìn)行的封裝,它也表示一個要被執(zhí)行的任務(wù),Operation對象有一個start()方法表示開始執(zhí)行這個任務(wù)。

不僅如此,Operation表示的任務(wù)還可以被取消。它還有三種狀態(tài)isExecuted、isFinished、isCancelled以方便我們通過 KVC 對它的狀態(tài)進(jìn)行監(jiān)聽。

想要開始執(zhí)行一個任務(wù)可以這么寫:

        let operation = BlockOperation.init { 
            print("初始化 0 的任務(wù)\(Thread.current)")
        }
        
        operation.addExecutionBlock {
            print("第 1 個添加任務(wù)\(Thread.current)")
        }
        operation.addExecutionBlock {
            print("第 2 個添加任務(wù)\(Thread.current)")
        }        
        operation.addExecutionBlock {
            print("第 3 個添加任務(wù)\(Thread.current)")
        }        
        operation.addExecutionBlock {
            print("第 4 個添加任務(wù)\(Thread.current)")
        }
        operation.addExecutionBlock {
            print("第 5 個添加任務(wù)\(Thread.current)")
        }        
        operation.addExecutionBlock {
            print("第 6 個添加任務(wù)\(Thread.current)")
        }        
        operation.addExecutionBlock {
            print("第 7 個添加任務(wù)\(Thread.current)")
        }       
        operation.addExecutionBlock {
            print("第 8 個添加任務(wù)\(Thread.current)")
        }     
        operation.start()
        print("結(jié)束了")
輸出內(nèi)容:
初始化 0 的任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 2 個添加任務(wù)<NSThread: 0x60000026af80>{number = 3, name = (null)}
第 1 個添加任務(wù)<NSThread: 0x608000262e80>{number = 5, name = (null)}
第 3 個添加任務(wù)<NSThread: 0x608000262d80>{number = 4, name = (null)}
第 4 個添加任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 5 個添加任務(wù)<NSThread: 0x60000026af80>{number = 3, name = (null)}
第 6 個添加任務(wù)<NSThread: 0x608000262e80>{number = 5, name = (null)}
第 8 個添加任務(wù)<NSThread: 0x60000007dc40>{number = 1, name = main}
第 7 個添加任務(wù)<NSThread: 0x608000262d80>{number = 4, name = (null)}
結(jié)束了

使用BlockOperation來創(chuàng)建是因為Operation是一個基類,不應(yīng)該直接生成Operation對象,而是應(yīng)該用它的子類。BlockOperation是蘋果預(yù)定義的子類,它可以用來封裝一個或多個 block ,后面會介紹如何自己創(chuàng)建Operation的子類。

在上面的例子里面我們創(chuàng)建了一個BlockOperation,并且設(shè)置好它的 block ,也就是將要執(zhí)行的任務(wù),同時,我們調(diào)用addExecutionBlock方法追加幾個任務(wù),這些任務(wù)會并行執(zhí)行。但是它并非是將所有的 block 都放到放到了子線程中。通過上面的打印記錄我們可以發(fā)現(xiàn),它會優(yōu)先將 block 放到主線程中執(zhí)行,若主線程已有待執(zhí)行的代碼,就開辟新的線程,但最大并發(fā)數(shù)為4(包括主線程在內(nèi),在真機(jī)上最大并發(fā)數(shù)為2,不必糾結(jié)這個,明白原理即可),如果 block 數(shù)量大于了線程的最大并發(fā)數(shù),那么剩下的 block 就會等待某個線程空閑下來之后被分配到該線程,且依然是優(yōu)先分配到主線程。

最后,調(diào)用start()方法讓Operation方法運行起來。start()是一個同步方法,也就是在調(diào)用start()方法的那個線程中直接執(zhí)行,會阻塞調(diào)用start()方法的線程。

OperationQueue

從上面我們可以知道Operation是同步執(zhí)行的。簡單的看一下 NSOperation 類的定義會發(fā)現(xiàn)它有一個只讀屬性 asynchronous,這意味著如果想要異步執(zhí)行,就需要自定Operation的子類?;蛘呤褂?code>OperationQueue。

OperationQueue類似于 GCD 中的隊列。我們知道 GCD 中的隊列有三種:主隊列、串行隊列并行隊列。OperationQueue更簡單,只有兩種:主隊列非主隊列。我們自己生成的OperationQueue對象都是非主隊列,主隊列可以用OperationQueue.main取得。

OperationQueue的主隊列是串行隊列,而且其中所有Operation都會在主線程中執(zhí)行。對于非主隊列來說,一旦一個Operation被放入其中,那這個Operation一定是并發(fā)執(zhí)行的。因為OperationQueue會為每一個Operation創(chuàng)建線程并調(diào)用它的start()方法。

OperationQueue有一個屬性叫maxConcurrentOperationCount,它表示最多支持多少個Operation并發(fā)執(zhí)行。如果maxConcurrentOperationCount被設(shè)為 1,就認(rèn)為這個隊列是串行隊列。需要注意的是設(shè)備最大并發(fā)數(shù)是有上限的,即使你設(shè)置maxConcurrentOperationCount為100,它也不會超過設(shè)備最大并發(fā)數(shù)上限,而這個上限的數(shù)目也是由具體運行環(huán)境決定的。

OperationQueueGCD中的隊列有這樣的對應(yīng)關(guān)系:

OperationQueue GCD
主隊列 OperationQueue.main DispatchQueue.main
串行隊列 自建隊列設(shè)置maxConcurrentOperationCount為 1 DispatchQueue(label: "serialQueue")
并發(fā)隊列 自建隊列設(shè)置maxConcurrentOperationCount大于 1 DispatchQueue(label: "concurrentQueue", qos: .default, attributes: .concurrent, autoreleaseFrequency: .workItem, target: nil

想要使用OperationQueue實現(xiàn)異步操作可以這么寫:

let operationQueue = OperationQueue()
let operation = BlockOperation()
        
for i in 1...10 {
    operation.addExecutionBlock {
        print("第 \(i) 個添加任務(wù)\(Thread.current)")
    }
}
operationQueue.addOperation(operation) 
輸出內(nèi)容:
第 1 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 2 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 3 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 4 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 5 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 6 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 7 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 8 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}
第 9 個添加任務(wù)<NSThread: 0x1702619c0>{number = 5, name = (null)}
第 10 個添加任務(wù)<NSThread: 0x170261880>{number = 4, name = (null)}

使用OperationQueue來執(zhí)行任務(wù)與之前的區(qū)別在于,首先創(chuàng)建一個非主隊列。然后用addOperation方法替換之前的start()方法。剛剛已經(jīng)說過,OperationQueue會為每一個Operation建立線程并調(diào)用他們的start()方法。

觀察一下運行結(jié)果,所有的Operation都沒有在主線程執(zhí)行,從而成功的實現(xiàn)了異步、并行處理。

除了上述的將Operation添加到隊列中的使用方法外,OperationQueue提供了一個更加簡單的方法,只需以下兩行代碼就能實現(xiàn)多線程調(diào)用

let operationQueue = OperationQueue()
operationQueue.addOperation {
    print(Thread.current)
}
輸出內(nèi)容:
<NSThread: 0x170460d40>{number = 4, name = (null)}

你可以同時添加一個或這個多個Block來實現(xiàn)你的操作。

取消任務(wù)

如果我們有兩次網(wǎng)絡(luò)請求,第二次請求會用到第一次的數(shù)據(jù)。假設(shè)此時網(wǎng)絡(luò)情況不好,第一次請求超時了,那么第二次請求也沒有必要發(fā)送了。而且用戶也有可能人為地取消某個Operation。

當(dāng)產(chǎn)生這種需求的時候,我們就可以取消這些操作:

//取消Operation
let operation = BlockOperation.init { 
    print("哈哈哈哈哈哈 0")
}
operation.cancel()

//取消某個OperationQueue剩余的Operation
let operationQueue = OperationQueue()
for i in 1...10 {
    operationQueue.addOperation {
       print(Thread.current)
    }
}
operationQueue.cancelAllOperations()

暫停和取消并不會立即暫?;蛉∠?dāng)前操作,而是不在調(diào)用新的Operation。

設(shè)置依賴

如果現(xiàn)在需要兩次網(wǎng)絡(luò)請求,第二次請求會用到第一次的數(shù)據(jù),所以我們要保證發(fā)出第二次請求的時候第一個請求已經(jīng)執(zhí)行完,但是我們同時還希望利用到OperationQueue的并發(fā)特性(因為可能不止這兩個任務(wù))。

這時候我們可以設(shè)置Operation之間的依賴關(guān)系:

//讓operation1在operation2執(zhí)行完之后執(zhí)行
let operationQueue = OperationQueue()

let operation1 = BlockOperation.init {
    print("第 1 個添加任務(wù)\(Thread.current)")
}        
let operation2 = BlockOperation.init {
    print("第 2 個添加任務(wù)\(Thread.current)")
}        
//需要注意的是Operation之間的相互依賴會導(dǎo)致死鎖,如果1依賴2,2又依賴1,就會導(dǎo)致死鎖。
operation1.addDependency(operation2)
        
operationQueue.addOperation(operation1)
operationQueue.addOperation(operation2)
輸入內(nèi)容:
第 2 個添加任務(wù)<NSThread: 0x17027a340>{number = 4, name = (null)}
第 1 個添加任務(wù)<NSThread: 0x17027a340>{number = 4, name = (null)}

OperationQueue暫停與恢復(fù)

暫停與恢復(fù)只需要操作isSuspended屬性:

operationQueue.isSuspended = true
operationQueue.isSuspended = false

Operation優(yōu)先級

每一個Operation的對象都一個queuePriority屬性,表示隊列優(yōu)先級。它是一個枚舉值,有這么幾個等級可選:

public enum QueuePriority : Int {
    case veryLow
    case low
    case normal
    case high
    case veryHigh
}

需要注意的是,這個優(yōu)先級并不總是起作用,不能完全保證優(yōu)先級高的任務(wù)一定先執(zhí)行,因為線程優(yōu)先級代表的是線程獲取CPU時間片的能力,高優(yōu)先級的執(zhí)行概率高,但是并不能確保優(yōu)先級高的一定先執(zhí)行。

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

最后編輯于
?著作權(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)容