Swift多執行緒:GCD進階,單例、訊號量、任務組

非典型技術宅發表於2018-02-23

其實這個標題不知道怎麼寫了,都很碎,也沒有想到特別合適的例子能夠全部放在一起的。索性就這麼平鋪開吧。

1. dispatch_once,以及Swift下的單例

使用dispatch_once函式能保證某段程式碼在程式執行過程中只被執行1次。所以在通常在OC時代,我們都會用它來寫單例。

但是,但是,但是:這個函式在Swift3.0以後的時代已經被刪除了。沒錯,被刪除了,不用了。

原來自從Swift 1.x開始Swift就已經開始用dispatch_one機制在後臺支援執行緒安全的全域性lazy初始化和靜態屬性。static var背後已經在使用dispatch_once了,所以從Swift 3開始,就乾脆把dispatch_once顯式的取消了。

凸(艹皿艹 ),那Swift裡面的單例怎麼寫吶?其實方法有很多種,有OC心Swift皮的寫法、新瓶裝老酒的寫法,那既然我們們開始了Swift,就拋下過去那寫沉重包袱吧。這裡非典型技術宅只分享其中的一種。

final class SingleTon: NSObject {
    static let shared = SingleTon()
    private override init() {}
}
複製程式碼

什麼?你在搞事情吧,就這麼點?是的,因為是全域性變數,所以只會建立一次。

  • 使用final,將這個單例類終止繼承。
  • 設定初始化方法為私有,避免外部物件通過訪問init方法建立單例類的例項。

2. dispatch_after

在GCD中我們使用dispatch_after()函式來延遲執行佇列中的任務。準確的理解是,等到指定的時間到了以後,才會開闢一個新的執行緒然後立即執行佇列中的任務。

所以dispatch_after不會阻塞當前任務,並不是先把任務加到執行緒裡面,等時間到了在執行。而是等時間了,才加入到執行緒中。

我們使用兩種時間格式來看看。

方法一:使用相對時間,DispatchTime

@IBAction func delayProcessDispatchTime(_ sender: Any) {
    //dispatch_time用於計算相對時間,當裝置睡眠時,dispatch_time也就跟著睡眠了.
    //Creates a `DispatchTime` relative to the system clock that ticks since boot.
    let time = DispatchTimeInterval.seconds(3)
    let delayTime: DispatchTime = DispatchTime.now() + time
    DispatchQueue.global().asyncAfter(deadline: delayTime) {
        Thread.current.name = "dispatch_time_Thread"
        print("Thread Name: \(String(describing: Thread.current.name))\n dispatch_time: Deplay \(time) seconds.\n")
    }
}
複製程式碼

方法二:使用絕對時間,DispatchWallTime

@IBAction func delayProcessDispatchWallTime(_ sender: Any) {
    //dispatch_walltime用於計算絕對時間。
    let delaytimeInterval = Date().timeIntervalSinceNow + 2.0
    let nowTimespec = timespec(tv_sec: __darwin_time_t(delaytimeInterval), tv_nsec: 0)
    let delayWalltime = DispatchWallTime(timespec: nowTimespec)
    
    //wallDeadline需要一個DispatchWallTime型別。建立DispatchWallTime型別,需要timespec的結構體。
    DispatchQueue.global().asyncAfter(wallDeadline: delayWalltime) {
        Thread.current.name = "dispatch_Wall_time_Thread"
        print("Thread Name: \(String(describing: Thread.current.name))\n dispatchWalltime: Deplay \(delaytimeInterval) seconds.\n")
    }
    
}
複製程式碼

3. 佇列的迴圈、掛起、恢復

3.1 dispatch_apply

dispatch_apply函式是用來迴圈來執行佇列中的任務的。在Swift 3.0裡面對這個做了一些優化,使用以下方法:

public class func concurrentPerform(iterations: Int, execute work: (Int) -> Swift.Void)
複製程式碼

本來迴圈執行就是為了節約時間的嘛,所以預設就是用了並行佇列。我們嘗試一下用這個升級版的dispatch_apply讓它執行10次列印任務。

@IBAction func useDispatchApply(_ sender: Any) {

        print("Begin to start a DispatchApply")
        DispatchQueue.concurrentPerform(iterations: 10) { (index) in
            
            print("Iteration times:\(index),Thread = \(Thread.current)")
        }
        
        print("Iteration have completed.")

}
複製程式碼

執行結果如下:

image.png

看,是不是所有的任務都是並行進行的?標紅的地方,是非典型技術宅想提醒一下大家這裡還是有一些任務是在主執行緒中進行的。它迴圈執行並行佇列中的任務時,會開闢新的執行緒,不過有可能會在當前執行緒中執行一些任務。

如果需要迴圈的任務裡面有特別耗時的操作,我們上一篇文章裡面說是應該放在global裡面的。如何避免在主執行緒操作這個吶???

來,給三秒時間想想。 看到呼叫這個方法的時候是不是就是在UI執行緒裡面這麼寫下來的嘛?那就開啟一個gloablQueue,讓它來進行不就好了嘛!BINGO! 這位同學,你已經深得真諦,可以放學後到我家後花園來了。嘿嘿✧(≖ ◡ ≖✿)嘿嘿

3.2 佇列的掛起與喚醒

如果一大堆任務執行著的時候,突然後面的任務不想執行的。那怎麼辦吶?我們可以讓它暫時先掛起,等想好了再讓它們執行起來。

不過掛起是不會暫停正在執行的佇列的哈,只能是掛起還沒執行的佇列。

@IBAction func useDispatchSuspend(_ sender: Any) {
    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() + DispatchTimeInterval.seconds(3)) {
        //            喚醒,開始執行
        queue.resume()
    }
}
複製程式碼

image.png

我們也可以看一下控制條的列印結果。顯然能看到程式碼並沒有按照順序執行,新建的queue裡面的列印是在被喚醒之後才執行的。

4. 訊號量(semaphore)

訊號量這個東西在之前的文章裡面有一個例子裡面用到了,當時還有人專門問我semaphore是什麼東西。現在可以好好說一說這個了。

不要問我是哪個例子裡面用到了,實在想不起來了呀,只能記得有人問過semaphore這個。

有時候多個執行緒對一個資料進行操作的時候,會造成一些意想不到的效果。多個人同時對同一個資料進行操作,誰知道怎麼搞啊!

為了保證同時只有一個執行緒來修改這個資料,這個時候我們就要用到訊號量了。當訊號量為0的時候,其他執行緒想要修改或者使用這個資料就必須要等待了,等待多久吶?DispatchTime.distantFuture,要等待這麼久。意思就是一直等待下去。。。。OC裡面管這個叫做DISPATCH_TIME_FOREVER

如果給訊號量設定成了0,其實就意味著這個資源沒有人能夠再能用了。所以,當用完了之後一定要把訊號量設定成非0( ⊙ o ⊙ )!

//建立一個訊號量,初始值為1
let semaphoreSignal = DispatchSemaphore(value: 1)

//表示訊號量-1
semaphoreSignal.wait()  

//表示訊號量+1
semaphoreSignal.signal() 
複製程式碼

4.1 簡單實用一下

我們簡單的讓globalQueue這個全域性佇列按照1->5的順序進行列印,列印一次休息1秒鐘。

@IBAction func useSemaphore(_ sender: Any) {
    let semaphoreSignal = DispatchSemaphore(value: 1)
    
    for index in 1...5 {
        DispatchQueue.global().async {
            semaphoreSignal.wait()
            print(Thread.current)
            print("這是第\(index)次執行.\n")
            semaphoreSignal.signal()
        }
        print("測試列印")
        
    }
    
}
複製程式碼

看一下列印結果:

image.png

globalQueue 如果不加訊號量,正常列印是什麼樣子的?如果不記得,請看上一篇文章。iOS多執行緒系列之三:使用GCD實現非同步下載圖片

好奇寶寶們有沒有想過,在建立訊號量的時候初始值設定成2或者更大的數,例如50,會是什麼效果? 自己敲敲程式碼試試嘍,想想看。

4.2 多個執行緒之間進行任務協調

實際工作中,很多時候我們需要在多個任務之間進行協調,每個任務都是多執行緒的。

打個比方,我們在後臺下載音樂、專輯的封面。等著兩個都做完了,才通知使用者可以去聽音樂了。兩個任務都是多執行緒,我們其實並不知道什麼時候才能執行完畢。這個時候,就可以靠訊號量,讓大家互相等待。

為了更簡化這個過程,例子裡面模擬了一個在另外一個方法中需要耗時1秒的一個操作。當完成之後,才執行後續操作。

func semaphoreDemo() -> Void {
    let sema = DispatchSemaphore.init(value: 0)
    getListData { (result) in
        if result == true {
            sema.signal()
        }
    }
    sema.wait()
    print("我終於可以開始幹活了")
}

private func getListData(isFinish:@escaping (Bool) -> ()) {
    
    DispatchQueue.global().async {
        Thread.sleep(forTimeInterval: 1)
        print("global queue has completed!")
        isFinish(true)
    }
    
}
複製程式碼

這個例子不是用group也可以做嘛?!是噠。也可以。

5. 任務組

GCD的任務組在開發中是經常被使用到,當需要一組任務結束後再執行一些操作時,就可以用它啦。

DispatchGroup的職責就是當佇列中的所有任務都執行完畢後,會發出一個通知來告訴告訴大家,任務組中所執行的佇列中的任務執行完畢了。

既然是組,裡面就肯定有很多佇列啦,不然怎麼能叫做“組”吶。

佇列和組關聯有兩種方式:手動、自動。

5.1 自動關聯

肯定先從自動開始了,因為通常自動最省事啊。這還用問嘛。

@IBAction func useGroupQueue(_ sender: UIButton) {
    let group = DispatchGroup()
    //模擬迴圈建立幾個全域性佇列
    for index in 0...3 {

//建立佇列的同時,加入到任務組中        
DispatchQueue.global().async(group: group, execute: DispatchWorkItem.init(block: {
            Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(2) + 1))
            print("任務\(index)執行完畢")
        }))
    }
    
    //組中所有任務都執行完了會傳送通知
    group.notify(queue: DispatchQueue.main) {
        print("任務組的任務都已經執行完畢啦!")
    }
    
    
    print("列印測試一下")
}
複製程式碼

看看列印結果:

image.png

5.2 手動關聯

接下來我們將手動的管理任務組與佇列中的關係。

enter()leave()是一對兒。前者表示進入到任務組。後者表示離開任務組。

let manualGroup = DispatchGroup()
//模擬迴圈建立幾個全域性佇列
for manualIndex in 0...3 {
    
    //進入佇列管理
    manualGroup.enter()
    DispatchQueue.global().async {
        //讓執行緒隨機休息幾秒鐘
        Thread.sleep(forTimeInterval: TimeInterval(arc4random_uniform(2) + 1))
        print("-----手動任務\(manualIndex)執行完畢")
        
        //配置完佇列之後,離開佇列管理
        manualGroup.leave()
    }
}

//傳送通知
manualGroup.notify(queue: DispatchQueue.main) {
    print("手動任務組的任務都已經執行完畢啦!")
}
複製程式碼

image.png

利用任務組可以完成很多場景的工作。例如多工執行完後,統一重新整理UI。把重新整理UI的操作放在notify裡面就好了。

還記得重新整理UI用哪個queue嘛?hoho~

最後,所有的程式碼都放在這裡了:gitHub 下載後給顆Star吧~ 麼麼噠~(~o ̄3 ̄)~ 愛你們~

相關文章