GCD是蘋果開發的多線程編程的解決方案,經過簡單的API就能夠實現建立新線程去執行咱們須要執行的任務,不須要咱們手動地建立和管理線程,只須要建立隊列和相應的函數配合使用就行。它的API包含在libdispatch庫中。程序員
GCD全稱Grand Central Dispatch,是Apple提供的一套底層API,提供了一種新的方法來進行併發程序編寫。GCD有點像NSOperationQueue,但它比NSOpertionQueue更底層更高效,而且它不屬於Cocoa框架。GCD的API很大程度上基於block,固然,GCD也能夠脫離block來使用,好比使用傳統C機制提供函數指針和上下文指針。實踐證實,當配合block使用時,GCD很是簡單易用且能發揮其最大能力。編程
任務(Task): 就是執行操做的意思,換句話說就是你在線程中執行的那段代碼。在 GCD 中是放在 block 中的。執行任務有兩種方式:**同步執行(sync)**和 異步執行(async)。二者的主要區別是:是否等待隊列的任務執行結束,以及是否具有開啓新線程的能力。api
隊列(Queue) 這裏的隊列指執行任務的等待隊列,即用來存聽任務的隊列。隊列是一種特殊的線性表,採用 FIFO(先進先出)的原則,即新任務老是被插入到隊列的末尾,而讀取任務的時候老是從隊列的頭部開始讀取。每讀取一個任務,則從隊列中釋放一個任務。安全
GCD中隊列的種類bash
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)}
...
複製代碼
能夠發如今並行隊列中, 不等待隊列的任務執行結束,具有開啓新線程的能力
若是想等到全部的隊列的任務執行完畢再進行後序操做時,可使用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
複製代碼
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
}
複製代碼
能夠理解爲隔離,仍是以文件讀寫爲例,在讀取文件時,能夠異步訪問,可是若是忽然出現了異步寫入操做,咱們想要達到的效果是在進行寫入操做的時候,使讀取操做暫停,直到寫入操做結束,再繼續進行讀取操做,以保證讀取操做獲取的是文件的最新內容。
先看看不使用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主要用於讀寫隔離,以保證寫入的時候,不被讀取。
DispatchSemaphore中的信號量,能夠解決資源搶佔的問題,支持信號的通知和等待.每當發送一個信號通知,則信號量+1;每當發送一個等待信號時信號量-1,若是信號量爲0則信號會處於等待狀態.直到信號量大於0開始執行.因此咱們通常將DispatchSemaphore的value設置爲1.
線程同步: 可理解爲線程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。
這樣就實現了線程同步,將異步執行任務轉換爲同步執行任務。
下面,咱們模擬火車票售賣的方式,實現 NSThread 線程安全和解決線程同步問題。
場景:總共有10張火車票,有兩個售賣火車票的窗口,一個是北京火車票售賣窗口,另外一個是上海火車票售賣窗口。兩個窗口同時售賣火車票,賣完爲止。
先來看看不考慮線程安全的代碼
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)}
全部票都售完了
全部票都售完了
複製代碼
線程安全的代碼
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中運行
爲總結學習而寫,如有錯誤,歡迎指正。