1. GCD 簡介
GCD是蘋果開發的多執行緒程式設計的解決方案,通過簡單的API就可以實現建立新執行緒去執行我們需要執行的任務,不需要我們手動地建立和管理執行緒,只需要建立佇列和相應的函式配合使用就行。它的API包含在libdispatch庫中。
GCD全稱Grand Central Dispatch,是Apple提供的一套底層API,提供了一種新的方法來進行併發程式編寫。GCD有點像NSOperationQueue,但它比NSOpertionQueue更底層更高效,並且它不屬於Cocoa框架。GCD的API很大程度上基於block,當然,GCD也可以脫離block來使用,比如使用傳統C機制提供函式指標和上下文指標。實踐證明,當配合block使用時,GCD非常簡單易用且能發揮其最大能力。
2. GCD 的優勢
- GCD是蘋果公司為多核的並行運算提出的解決方案
- GCD會自動利用更多的CPU核心(比如雙核、四核)
- GCD會自動管理執行緒的生命週期(建立執行緒、排程任務、銷燬執行緒)
- 程式設計師只需要告訴GCD想要執行什麼任務,不需要編寫任何執行緒管理程式碼
3. GCD 任務和佇列
任務(Task): 就是執行操作的意思,換句話說就是你線上程中執行的那段程式碼。在 GCD 中是放在 block 中的。執行任務有兩種方式:**同步執行(sync)**和 非同步執行(async)。兩者的主要區別是:是否等待佇列的任務執行結束,以及是否具備開啟新執行緒的能力。
- 同步執行(sync):
- 同步新增任務到指定的佇列中,在新增的任務執行結束之前,會一直等待,直到佇列裡面的任務完成之後再繼續執行。
- 只能在當前執行緒中執行任務,不具備開啟新執行緒的能力。
- 非同步執行(async):
- 非同步新增任務到指定的佇列中,它不會做任何等待,可以繼續執行任務。
- 可以在新的執行緒中執行任務,具備開啟新執行緒的能力(但是並不一定開啟新執行緒, 跟任務所指定的佇列型別有關)。
佇列(Queue) 這裡的佇列指執行任務的等待佇列,即用來存放任務的佇列。佇列是一種特殊的線性表,採用 FIFO(先進先出)的原則,即新任務總是被插入到佇列的末尾,而讀取任務的時候總是從佇列的頭部開始讀取。每讀取一個任務,則從佇列中釋放一個任務。
GCD中佇列的種類
- 序列佇列(Serial Dispatch Queue): 每次只有一個任務被執行。讓任務一個接著一個地執行。(只開啟一個執行緒,一個任務執行完畢後,再執行下一個任務)
- 併發佇列(Concurrent Dispatch Queue): 可以讓多個任務併發(同時)執行。(可以開啟多個執行緒,並且同時執行任務), 併發佇列的併發功能只有在非同步(dispatch_async)函式下才有效
4.GCD 的簡單使用
- 建立一個佇列(序列佇列或併發佇列)
- 將任務追加到任務的等待佇列中,然後系統就會根據任務型別執行任務(同步執行或非同步執行)
GCD建立佇列
-
主佇列(序列佇列)
let mainQueue = DispatchQueue.main 複製程式碼
-
全域性並行佇列
let globalQueue = DispatchQueue.global(qos: .default) 複製程式碼
-
建立序列佇列
let serialQueue = DispatchQueue(label: "vip.mybadge") 複製程式碼
-
建立並行佇列
let concurQueue = DispatchQueue(label: "vip.mybadge", attributes: .concurrent) 複製程式碼
執行任務
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)}
...
複製程式碼
可以發現在序列佇列中, 等待佇列的任務執行結束,不具備開啟新執行緒的能力
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)}
...
複製程式碼
可以發現在並行佇列中, 不等待佇列的任務執行結束,具備開啟新執行緒的能力
5. DispatchGroup
如果想等到所有的佇列的任務執行完畢再進行後序操作時,可以使用DispatchGroup來完成。
let group = DispatchGroup()
for i in 0..<5 {
print("任務(i+1)下載中...")
DispatchQueue.global().async(group: group) {
Thread.sleep(forTimeInterval: 1)
print("任務(i+1)下載完成")
}
}
group.notify(queue: DispatchQueue.main) {
print("任務都下載完成...去更新UI")
}
複製程式碼
執行結果
任務1下載中...
任務2下載中...
任務3下載中...
任務4下載中...
任務5下載中...
任務1下載完成
任務3下載完成
任務5下載完成
任務4下載完成
任務2下載完成
任務都下載完成...去更新UI
複製程式碼
6. DispatchWorkItem
Swift3新增的類,可以通過此類設定佇列執行的任務。相當於把原來GCD中閉包的程式碼封裝到了這裡,
看一個例子:
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也可以設定優先順序,另外還有個引數DispatchWorkItemFlags,來看看DispatchWorkItemFlags的內部組成:
public struct DispatchWorkItemFlags : OptionSet, RawRepresentable {
// 相當於之前的柵欄函式
public static let barrier: DispatchWorkItemFlags
public static let detached: DispatchWorkItemFlags
public static let assignCurrentContext: DispatchWorkItemFlags
// 沒有優先順序
public static let noQoS: DispatchWorkItemFlags
// 繼承Queue的優先順序
public static let inheritQoS: DispatchWorkItemFlags
// 覆蓋Queue的優先順序
public static let enforceQoS: DispatchWorkItemFlags
}
複製程式碼
7. Dispatch barrier
可以理解為隔離,還是以檔案讀寫為例,在讀取檔案時,可以非同步訪問,但是如果突然出現了非同步寫入操作,我們想要達到的效果是在進行寫入操作的時候,使讀取操作暫停,直到寫入操作結束,再繼續進行讀取操作,以保證讀取操作獲取的是檔案的最新內容。
先看看不使用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)
}
複製程式碼
輸出結果
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.
複製程式碼
我們期望的結果是,在寫檔案之前,列印 “This is a file”, 寫檔案之後列印的是”This file has been modified.”, 上面結果顯然不是我們想要的。
看一下使用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)
}
複製程式碼
輸出結果
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.
複製程式碼
結果符合預期的想法,barrier主要用於讀寫隔離,以保證寫入的時候,不被讀取。
8. DispatchSemaphore
DispatchSemaphore中的訊號量,可以解決資源搶佔的問題,支援訊號的通知和等待.每當傳送一個訊號通知,則訊號量+1;每當傳送一個等待訊號時訊號量-1,如果訊號量為0則訊號會處於等待狀態.直到訊號量大於0開始執行.所以我們一般將DispatchSemaphore的value設定為1.
DispatchSemaphore 執行緒同步
執行緒同步: 可理解為執行緒A和執行緒B一塊配合, A執行到一定程度時要依靠B的某個結果, 於是停下來, 示意B執行; B依言執行, 再將結果給A; A再繼續操作.
/// 訊號量的執行緒同步.
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 是在執行完 number = 100; 之後才列印的。而且輸出結果 number 為 100。
- 這是因為非同步執行不會做任何等待,可以繼續執行任務。
- 非同步執行將workItem追加到佇列之後,不做等待,接著去執行**semaphoreSignal.wait()**方法。
- 此時 semaphore == 0,當前執行緒進入等待狀態。
- 然後,workItem開始執行。workItem執行到**semaphoreSignal.signal()**之後,
- 訊號量+1,此時 semaphore == 1,**semaphoreSignal.wait()**方法使總訊號量減1,正在被阻塞的執行緒(主執行緒)恢復繼續執行。
- 最後列印number = 100,semaphore—end,。
這樣就實現了執行緒同步,將非同步執行任務轉換為同步執行任務。
Dispatch Semaphore 執行緒安全和執行緒同步(為執行緒加鎖)
- 執行緒安全:如果你的程式碼所在的程式中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。否則就是 非執行緒安全的。
下面,我們模擬火車票售賣的方式,實現 NSThread 執行緒安全和解決執行緒同步問題。
場景:總共有10張火車票,有兩個售賣火車票的視窗,一個是北京火車票售賣視窗,另一個是上海火車票售賣視窗。兩個視窗同時售賣火車票,賣完為止。
非執行緒安全(不使用semaphore)
先來看看不考慮執行緒安全的程式碼
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("剩餘票數(ticketSurplusCount), 視窗:(Thread.current)")
Thread.sleep(forTimeInterval: 1)
} else {
print("所有票都售完了")
break
}
}
}
}
let saleTicket = SaleTicketNotSafe(ticketSurplusCount: 10)
saleTicket.startSaleNotSave()
複製程式碼
輸出結果
開始售票 thread=<NSThread: 0x600003802900>{number = 1, name = main}
剩餘票數9, 視窗:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩餘票數8, 視窗:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩餘票數6, 視窗:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩餘票數7, 視窗:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩餘票數4, 視窗:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩餘票數4, 視窗:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩餘票數3, 視窗:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩餘票數2, 視窗:<NSThread: 0x600003824c00>{number = 6, name = (null)}
剩餘票數1, 視窗:<NSThread: 0x6000038157c0>{number = 4, name = (null)}
剩餘票數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("開始售票 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("剩餘票數(ticketSurplusCount), 視窗:(Thread.current)")
Thread.sleep(forTimeInterval: 1)
} else {
semaphoreSignal.signal()
print("所有票都售完了")
break
}
semaphoreSignal.signal()
}
}
}
let saleTicket = SaleTicketSafe(ticketSurplusCount: 10)
saleTicket.startSaleSave()
複製程式碼
輸出結果
開始售票 thread=<NSThread: 0x600001ac6900>{number = 1, name = main}
剩餘票數9, 視窗:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩餘票數8, 視窗:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩餘票數7, 視窗:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩餘票數6, 視窗:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩餘票數5, 視窗:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩餘票數4, 視窗:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩餘票數3, 視窗:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩餘票數2, 視窗:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
剩餘票數1, 視窗:<NSThread: 0x600001ad4b80>{number = 4, name = (null)}
剩餘票數0, 視窗:<NSThread: 0x600001ad8640>{number = 6, name = (null)}
所有票都售完了
所有票都售完了
複製程式碼
可以看出,在考慮了執行緒安全的情況下,使用 DispatchSemaphore 機制之後,得到的票數是正確的,沒有出現混亂的情況。我們也就解決了執行緒安全與執行緒同步的問題。
以上
以上程式碼可以直接在Playground中執行
為總結學習而寫,若有錯誤,歡迎指正。