iOS - 多執行緒分析之 DispatchQueue Ⅰ

一個絕望的氣純發表於2019-01-24

Dispatch ( 全稱 Grand Central Dispatch,簡稱 GCD ) 是一套由 Apple 編寫以提供讓程式碼以多核併發的方式執行應用程式的框架。

DispatchQueue ( 排程佇列 ) 就是被定義在 Dispatch 框架中,可以用來執行跟多執行緒有關操作的類。

在使用它之前,我們得先了解一下基本概念,我會先簡單介紹,後面再根據講解的內容逐步詳細介紹,目的是為了方便讀者融入。

PS:如果在閱讀時發現有任意錯誤,請指點我,感謝!

同步和非同步執行

同步和非同步執行

如圖。同步和非同步的區別在於,執行緒會等待同步任務執行完成;執行緒不會等待非同步任務執行完成,就會繼續執行其他任務/操作。

閱讀指南:

本文中出現的 "任務" 是指 sync {}async {} 中整個程式碼塊的統稱,"操作" 則是在 "任務" 中執行的每一條指令 ( 程式碼 ) ;因為主執行緒沒有 "任務" 之說,主執行緒上執行的每一條 ( 段 ) 程式碼,都統稱為 "操作"。

序列和併發佇列

在 GCD 中,任務由**佇列 (序列或併發) **負責管理和決定其執行順序,在一條由系統自動分配的執行緒上執行。

序列 (Serial) 佇列中執行任務時,任務會按照固定順序執行,執行完一個任務後再繼續執行下一個任務 (這意味著序列佇列同時只能執行一個任務) ;在併發 (Concurrent) 佇列中執行任務時,任務可以同時執行 ( 其實是在以極短的時間內不斷的切換執行緒執行任務 ) 。

序列和併發佇列都以 先進先出 (FIFO) 的順序執行任務,任務的執行流程如圖:

任務執行流程

示例1 - 在序列佇列中執行同步 ( sync ) 任務

// 建立一個佇列(預設就是序列佇列,不需要額外指定引數)
let queue = DispatchQueue(label: "Serial.Queue")

print("thread: \(Thread.current)")

queue.sync {
    (0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}

queue.sync {
    (0..<5).forEach { print("rool-1 -> \($0): \(Thread.current)") }
}

/**
 thread: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-1 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 0: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 1: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 2: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 3: <NSThread: 0x281951f40>{number = 1, name = main}
 rool-2 -> 4: <NSThread: 0x281951f40>{number = 1, name = main}
 */
複製程式碼

沒什麼好解釋的,結果肯定是按照正常的順序來,一個接著一個地執行。因為同步執行就是會一直等待,等到一個任務全部執行完成後,再繼續執行下一個任務。

有一點需要注意的是,主執行緒和在同步任務中 Thread,current 的列印結果相同,也就是說,佇列中的同步任務在執行時,系統給它們分配的執行緒是主執行緒,因為同步任務會讓執行緒等待它執行完,既然會等待,那就沒有再開闢執行緒的必要了。

關於主執行緒和主佇列

當應用程式啟動時,就有一條執行緒被系統建立,與此同時這條執行緒也會立刻執行,該執行緒通常叫做程式的主執行緒

同時系統也為我們提供一個名為主佇列 ( DispatchQueue.main {} ) 的序列特殊佇列,預設我們寫的程式碼都處於主佇列中,主佇列中的所有任務都在主執行緒執行。

示例2 - 在序列佇列中執行非同步 ( async ) 任務

let queue = DispatchQueue(label: "serial.com")

print("thread: \(Thread.current)")

(0..<50).forEach {
    print("main - \($0)")
    // 讓執行緒休眠0.2s,目的是為了模擬耗時操作,不再贅述。
    Thread.sleep(forTimeInterval: 0.2)
}

queue.async {
    (0..<5).forEach {
        print("rool-1 -> \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

queue.async {
    (0..<5).forEach {
        print("rool-2 -> \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

/**
 thread: <NSThread: 0x281251fc0>{number = 1, name = main}
 main - 0
 main - 1
 main - 2 ... 順序執行到 49
 rool-1 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-1 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 0: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 1: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 2: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 3: <NSThread: 0x281234100>{number = 3, name = (null)}
 rool-2 -> 4: <NSThread: 0x281234100>{number = 3, name = (null)}
 */
複製程式碼

可以看到,執行緒一定會等待它當前的操作 ( 包括讓執行緒休眠 ) 執行完後,再繼續執行 async 任務。此時任務同樣按順序執行,因為序列佇列只能執行完一個任務後再繼續執行下一個任務。

任務中 Thread.current 的列印結果都是 number = 3 ,換句話說,序列佇列中的非同步任務在執行時,系統給它們開闢的執行緒是其他執行緒,並且只開闢一個,因為序列佇列同時只能執行一個任務,因此沒有開啟多條執行緒的必要。

關於讓執行緒休眠

這裡解釋一下 Thread.sleep 這個方法的作用:是讓當前執行緒暫停任何操作0.2s。

請注意我說的是當前執行緒不要誤以為是讓整個應用程式都停止了,不是這樣的。如果當前任務所在的執行緒停止了,是不會影響到別的執行緒正在執行任務的,這點要區分清楚。

PS:也就是說,在上面同步任務中,為了測試而呼叫的 Thread.sleep 方法並沒有作用 ( 但是為了測試和驗證,依然呼叫了 ) ,因為任務都在一條執行緒上,並按照固定順序執行。

示例3 - 在序列佇列中執行非同步 ( async ) 任務 II

let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2: \(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")

/**
 1: <NSThread: 0x28347ed00>{number = 1, name = main}
 3: <NSThread: 0x28347ed00>{number = 1, name = main}
 2: <NSThread: 0x2834268c0>{number = 3, name = (null)}
 5: <NSThread: 0x28347ed00>{number = 1, name = main}
 4: <NSThread: 0x2834268c0>{number = 3, name = (null)}
 */
複製程式碼

這時候列印的順序並不固定,但肯定會先從 1 開始列印,列印的結果可能是:12345, 12354, 13254, 13245, 13524, 13254... ,這是為什麼?我們先來了解一些概念後再來回顧。

佇列和任務的關係

首先要解釋一下同步非同步這兩個詞的概念,既然是同步或非同步,也能解釋為相同,或是不同,它需要一個作為參照的物件,來知道它們相對於這個物件來說到底是相同,還是不同。

那在 GCD 中,它們的參照物件就是我們的主執行緒 ( dispatchQueue.main ) 。也就是說如果是同步任務,那就在主執行緒執行;而如果是非同步任務,那就在其他執行緒執行

這就解釋了,為什麼序列佇列在執行非同步任務時,還會開啟執行緒,所謂非同步嘛,那就是不在主執行緒執行,區別是序列佇列只會開啟一條執行緒,而併發佇列會開啟多條執行緒

而同步任務是,甭管它是什麼佇列和任務,只要執行的是同步任務,就在主執行緒執行

  • 非同步任務

    非同步任務說:“我要開始執行任務了,快給我分配執行緒讓我執行。”

    應用程式說:“好!我另外開闢執行緒出來讓你執行,等等,請問你所處的佇列是?”

    非同步任務說:“序列佇列。”

    應用程式說:“既然是序列佇列,而序列佇列中的所有任務都會按照固定順序執行,只能執行完一個任務後再繼續執行下一個任務 ( 這意味著序列佇列同時只能執行一個任務 ) ,那我就只給你分配一條執行緒吧!你佇列中的所有任務、包括你,都在這條執行緒上順序執行。”

    非同步任務說:“那如果我處在併發佇列中呢?”

    應用程式說:“如果是在併發佇列中,那佇列中的所有任務可以同時執行,我會給你分配多條執行緒,讓每個任務可以在不同的執行緒上同時執行。”

  • 同步任務

    同步任務說:“我要開始執行任務了,快給我分配執行緒讓我執行。”

    應用程式說:“既然是同步任務那就相當於在主執行緒執行,那我就給你主執行緒來執行吧!”

    同步任務說:“我的待遇太差了。”

任務和執行緒的關係

任務只有兩種,同步任務和非同步任務,無論同步任務是處在什麼佇列中,它都會讓當前正在執行的執行緒等待它執行完成,例如:

// 當前執行緒執行列印 main-1 的操作
print("main-1")

// 執行緒執行到這裡發現遇到一個 sync 任務,就會在此等待,
// 直到 sync 任務執行完成,才會繼續執行其他操作。
//
// 序列或併發佇列
queue.sync {
    (0..<10).forEach {
        print("sync \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.5)
    }
}
// 等待!執行緒等待 sync 執行完後,再繼續執行列印 main-2 的操作。
print("main-2")

/**
 main-1
 sync 0: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 1: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 2: <NSThread: 0x6000011968c0>{number = 1, name = main}
 sync 2 ...9
 main-2
*/
複製程式碼

而如果是非同步任務,不管它處在什麼佇列中,當前執行緒都不會等待它執行完成,例如:

// 當前執行緒執行列印 main-1 的操作
print("main-1")

// 執行緒執行到這裡發現遇到一個 async 任務,
// 那麼執行緒不會等待它執行完成,就會繼續執行其他操作。
//
// 序列或併發佇列
queue.async {
    (0..<20).forEach { print("async \($0)") }
}

// 開闢執行緒的時間大約是90微妙,加上迴圈的準備以及列印時間,
// 這裡給它200微妙,測試async任務中的執行緒和當前執行緒之間的執行順序。
Thread.sleep(forTimeInterval: 0.0002000)

// 不會等待!執行緒不會等待 async 執行完成就會執行列印 main-2 的操作
print("main-2")
複製程式碼

列印的結果可能稍有不同,但是肯定先從 main-1 開始列印。雖然 main-2 是執行在 async 後面的,async 也會先執行,但是由於當前執行緒不等待它執行完成的機制,所以它在執行到某一刻時如果到了執行緒需要列印 main-2 的時間,就會執行列印 main-2 的操作。也有可能是,main-2 先執行,然後等到了某一時刻再執行 async 中的任務 ( 開闢執行緒需要時間 ) 。

也就是說,這裡當前執行緒和 async 任務中的執行緒在執行時是不阻塞對方的 ( 互不等待 ) ,本次執行結果如下:

/**
main-1
async 0
async 1
async 2
main-2
async 3
async 4
async 5
...
*/
複製程式碼

PS:我是怎麼知道開闢執行緒的時間大約是 90 微妙的?因為我看了執行緒成本中的描述。

回顧

這就能解釋之前示例中的執行順序了,再來回顧一下:

let queue = DispatchQueue(label: "serial.com")
print("1: \(Thread.current)")
queue.async { print("2-\(Thread.current)") }
print("3: \(Thread.current)")
queue.async { print("4: \(Thread.current)") }
print("5: \(Thread.current)")
複製程式碼

雖然執行順序不固定,但還是有一定的規律可循的,因為是序列佇列,所以在主執行緒中 1, 3, 5 一定按順序執行,而在 async 執行緒中 2, 4 也一定按順序執行。

示例4 - 序列佇列死鎖

首先,併發佇列不會出現死鎖的情況;其次,在序列佇列中,只有 sync { sync {} }async { sync {} } 會出現死鎖,內部的 sync closure 永遠不會被執行,並且程式會崩潰,例如:

queue.sync {
    print("1")
    queue.sync { print("2") }
    print("3")
}
// Prints "1"

queue.async {
    print("1")
    queue.sync { print("2") }
    print("3")
}
// Prints "1"
複製程式碼

仔細觀察上面的程式碼就會發現,只有內部套用 sync {} 的情況下才會死鎖,那使用 sync ( 同步 ) 意味著什麼呢?這意味著,當前執行緒會等待同步任務執行完成。可問題是,這個 sync 任務是巢狀在另一個任務裡面的 ( sync { sync {} } ) ,那這裡就有兩個任務了。

由於序列佇列是執行完當前任務後,再繼續執行下一個任務。放到這裡就是,內部的 sync {} 想要執行的話,它必須要等待外部的 sync {} 執行完成,那外部的 sync {} 能不能執行完成呢?由於這個內部任務是同步的,它會阻塞當前正在執行外部 sync {} 的執行緒,讓當前執行緒等待它 ( 內部 sync {} ) 執行完成,可問題是外部的 sync {} 完成不了的話,內部的 sync {} 也無法執行,結果就是一直等待,誰都無法繼續執行,造成死鎖。

既然執行緒會等待內部的同步任務執行完成,又限制序列佇列同時只能執行一個任務,那在外部的 sync {} 沒有執行完成之前,內部的 sync {} 永遠不能執行,而外部執行緒在等待內部 sync {} 執行完成的條件下,導致外部的 sync {} 也無法執行完成。

總結:因為序列佇列同時只能執行一個任務,就意味著無論如何,執行緒只能先執行完當前任務後,再繼續執行下一個任務。而同步任務的特點是,會讓執行緒等待它執行完成。那問題就來了,我 ( 執行緒 ) 既不可能先去執行它,又要等待它,結果是導致外部任務永遠無法執行完成,而內部的任務也永遠無法開啟。

對於第二段程式碼 async { sync {} } 的死鎖,原理是一樣的,不要被它外部的 async {} 給迷惑了,內部的 sync {} 同樣會阻塞它的執行緒執行,阻塞的結果就是外部的 async {} 無法執行完成,內部的 sync {} 也永遠無法開啟。

至於序列佇列另外兩種任務的巢狀結構 sync { async {} }async { async } ,例如:

queue.sync {
    print("task-1")
    queue.async {
        (0..<10).forEach {
            print("task-2: \($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("task-1 - end")
}
/**
 1
 task-1 - end
 task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 2 ... 9
*/

queue.sync {
    print("task-1")
    queue.async {
        (0..<10).forEach {
            print("task-2: \($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("task-1 - end")
}
/**
 1
 task-1 - end
 task-2: 0 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 1 <NSThread: 0x6000019c0d80>{number = 3, name = (null)}
 task-2: 2 ... 9
*/
複製程式碼

雖然已經不再死鎖,但執行的順序稍有不同,可以看到,程式是先把外部任務執行完後,再去執行內部任務。這是因為,內部的 async {} 已經不再阻塞當前執行緒,又因為序列佇列只能先把當前任務執行完後,再去執行下一個任務,那自然而然就是先把外部任務執行完後,再接著去執行內部的 async {} 任務了。

示例5 - DispatchQueue.main 特殊序列主佇列

前面說過,async 中的任務都會在其他執行緒執行,那對於主佇列中的 async 呢?在專案中我們經常呼叫的 DispatchQueue.main.asyncAfter(deadline:) 難道是在其他執行緒執行嗎?其實不是的,如果是 DispatchQueue.main 自己的佇列,那麼即使是 async ,也會在主執行緒執行,由於主佇列本身是序列佇列,也是同時只能執行一個任務,所以是,它會在處理完當前任務後,再去處理 async 中的任務,例如:

// 實際上相當於在 DispatchQueue.main.sync {} 中執行
print("1")

DispatchQueue.main.async {
    (0..<10).forEach { 
        print("async\($0) \(Thread.current)") 
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("3")

/**
 1
 3
 async0 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async1 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async2 <NSThread: 0x6000007928c0>{number = 1, name = main}
 async3 ...9
 */
複製程式碼

雖然 async 不阻塞當前執行緒執行,但是由於都在一個佇列上,DispatchQueue.main 只能先執行完當前任務後,再繼續執行下一個任務 ( async ) 。

而如果在主執行緒呼叫 DispatchQueue.main.sync {} 又會如何呢?答案是:會死鎖。其實原因很簡單,因為整個主執行緒的程式碼就相當於放在一個大的 DispatchQueue.main.sync {} 任務中,這時候如果再呼叫 DispatchQueue.main.sync {} ,結果肯定是死鎖。

還有一點需要留意,一定要在主執行緒執行和有關 UI 的操作,如果是在其他執行緒執行,例如:

queue.async {	// 併發佇列
    customView.backgroundColor = UIColor.blue
}
複製程式碼

很可能就會接收到一個 Main Thread Checker: UI API called on a background thread: -[UIView setBackgroundColor:] 的崩潰報告,因此主執行緒也被稱為 UI 執行緒

示例6 - 在併發佇列中執行同步 ( sync ) 任務

let queue = DispatchQueue(label: "serial.com", attributes: .concurrent)

queue.sync {
    (0..<10).forEach {
        print("task-1 \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("main-1")

queue.sync {
    (0..<10).forEach {
        print("task-2 \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.2)
    }
}

print("main-2")

/**
 task-1 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-1 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
 main-1
 task-2 0: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 1: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 2: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 3: <NSThread: 0x6000023968c0>{number = 1, name = main}
 task-2 4: <NSThread: 0x6000023968c0>{number = 1, name = main}
 main-2
 */
複製程式碼

使用併發佇列執行同步任務和在主執行緒執行操作並沒有區別,因為 sync 會牢牢的將當前執行緒固定住,讓執行緒等待它執行完成後才能繼續執行其他操作。這裡也能夠看到,main-1main-2 分別等待 sync 執行結束後才能執行。

示例7 - 在併發佇列中執行非同步 ( async ) 任務

線上程將要執行到某個佇列的 async 時,佇列才會開始併發執行任務,執行緒不可能跨越當前正在執行的操作去啟動任務。舉個例子:

// 指定為建立併發佇列 (.concurrent)
let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)

(0..<100).forEach {
    print("main-\($0)")
    Thread.sleep(forTimeInterval: 0.02)
}

queue.async { print("task-1", Thread.current) }
queue.async { print("task-2", Thread.current) }
queue.async { print("task-3", Thread.current) }
queue.async { print("task-4", Thread.current) }
queue.async { print("task-5", Thread.current) }
queue.async { print("task-6", Thread.current) }

print("main-end")

/**
 main-0
 main-1
 main-2 ...99
 task-2 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-4 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-5 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 task-3 <NSThread: 0x282e38800>{number = 5, name = (null)}
 task-6 <NSThread: 0x282e387c0>{number = 3, name = (null)}
 print("main-end")
 task-1 <NSThread: 0x282e04b40>{number = 4, name = (null)}
*/
複製程式碼

因為主執行緒也是序列佇列,程式將按照順序執行,等到所有迴圈執行完成後,才會執行 queue.async ,由於是併發佇列,所有任務都會同時執行,執行順序並不固定,而最後的 main-end 可能安插在佇列中某個任務完成前後的地方。

因為在執行 main-end 之前,任務已經被佇列併發出去了。對於主執行緒來說,它完成列印 main-end 的時間是固定的,但是佇列中併發任務的執行完成的時間並不固定 ( 執行任務會消耗時間 ) 。這時主執行緒並不會等待 async 的所有任務執行結束就會繼續執行列印 main-end 的操作。

所以是,如果在執行 async 的某個時間內剛好到了主執行緒需要列印 main-end 的時間,就會執行列印 main-end 的操作,而 async 中還沒有完成的任務將會繼續執行,如圖:

併發的時機

可以看到,迴圈操作結束後,佇列才開始併發執行任務,列印 main-end 的操作在 queue.async 之後執行,但是由於佇列執行任務需要時間,所以 main-end 有可能在 queue.async 執行完成之前執行。

對於一條執行緒來說,它的所有操作絕對按照固定順序執行,不存在一條執行緒同時執行多個任務的情況。而我們的所謂併發,就是給每個任務開闢一條執行緒出來執行,等到有某個執行緒執行完後,就會複用這條執行緒去執行其他在佇列中還沒有開始執行的任務。

一條執行緒只負責執行它當前任務中的所有操作,至於其他執行緒被開啟後 ( 前提是不要開啟同樣的執行緒 ) ,它們就在各自的執行緒上分別獨立執行任務,互不影響。舉個例子:

假設你要跑100米,當跑到50米的時候,就會有5個人跟你一起跑,跑到終點的時候,可能是你跑得比他們都快,也有可能是他們之中的任意人跑得比你快。

那你就可以想象成那 "5個人" 就是併發中的任務 ( 同時執行) ,而 "你" 就是當前執行緒。

示例8 - 併發佇列的疑惑 - sync { sync {} }

那什麼時候會開啟同樣的執行緒呢?也就是說,假設有一條執行緒 3 在執行,那麼在這條執行緒 3 還沒有執行完成的時候,就又有一條執行緒為 3 的任務開啟了。這對於 async 任務來說,幾乎不可能 ( 我說幾乎是因為我不確定,按照我的猜測,應該不會出現這種情況 ) ,也就是說,想要開啟同樣的一條執行緒執行非同步任務,必須要等到前面的執行緒執行完後,再用這條執行緒去執行其他任務。

但是對於 sync 任務來說,在 sync 還沒執行完的時候,我可以在 sync {} 內部又開啟一個 sync {} 任務,因為 sync {} 註定在主執行緒執行 ( async 任務無法指定在哪一條執行緒執行,而是由系統自動分配 ) ,這樣一來,就有了在一條執行緒還沒有執行完的時候,就又有一條同樣的執行緒開啟執行任務了。在序列佇列中,我們已經知道,這樣做會造成死鎖,那在併發佇列中又會如何呢?例如:

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
queue.sync {
    print("sync-start")
    queue.sync {
        (0..<5).forEach {
            print("task \($0): \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.5)
        }
    }
    print("sync-end")
}

/**
 sync-start
 task 0: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 1: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 2: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 3: <NSThread: 0x600003b828c0>{number = 1, name = main}
 task 4: <NSThread: 0x600003b828c0>{number = 1, name = main}
 sync-end
 */
複製程式碼

我們已經看到結果,任務按照順序執行,內部 sync 會阻塞外部 sync 我們也會清楚,問題是在外部的 sync {} 還沒有執行完的時候,為什麼內部的 sync 可以執行?

首先要了解最重要的一點,那就是,為什麼在序列佇列中內部的 sync {} 無法執行?最重要的原因在於序列佇列同時只能執行一個任務,所以在它上一個任務 ( 外部 sync ) 還沒有執行完成之前,它是不能執行下一個任務 ( 內部 sync ) 的。

而併發佇列就不同了,併發佇列可以同時執行多個任務。也就是說,內部的 sync 已經不用等待外部 sync 執行完成就可以執行了。但是由於是同步任務,所以還是會等待,等待內部 sync 執行完成後,外部的 sync 繼續執行。

請注意這裡的執行和上面所說的,不存在一條執行緒同時執行多個任務的情況並不矛盾。因為在執行內部 sync 時,外部執行緒就停止操作了 ( 其實是轉去執行內部 sync 了 ) ,如果是在執行內部 sync 的同時,外部的 sync 還在繼續執行操作,那才叫同時

因為 sync 都在一個執行緒 ( 主執行緒 ) 上,所以當你指定任務為 sync 時,主執行緒就知道接下來要去執行 sync 任務了,等執行完這個 sync 後再執行其他操作。例如,你可以把 sync 想象成是一個方法:

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)

queue.sync {
    print("sync-start")
	queueSync()
    print("sync-end")
}

// 相當於之前的 queue.sync {}
func queueSync() {
    (0..<5).forEach {
        print("task \($0): \(Thread.current)")
        Thread.sleep(forTimeInterval: 0.5)
    }
}
複製程式碼

關於先進先出 (FIFO)

對序列佇列來說,先進先出的意思很好理解,先進先出就是,先進去的一定先執行。當我們要執行一些任務時,這些任務就被儲存在它的佇列中,當執行緒進入到任務程式碼塊時,就一定會先把這個任務執行完,再將任務出列,等這個任務出列後,執行緒才能繼續去執行下一個任務。

那對於併發佇列也是一樣,當不同的執行緒同時進入到任務程式碼塊時,就一定會先把這些任務執行完,再將這些任務出列,然後這些執行緒才能繼續去執行其他任務。

示例9 - 關於併發的個數和執行緒效能

let queue = DispatchQueue(label: "concurrent.com", attributes: .concurrent)
(0..<100).forEach { i in
    queue.async { print("\(i) \(Thread.current)") }
}
複製程式碼

會怎麼樣?答案是不會怎麼樣,只是會開啟很多執行緒來執行這些非同步任務。前面說過,每一個非同步任務都是在不同的執行緒上執行的,那如果同時執行很多非同步任務的話,像我們這裡,同時開啟 100 個非同步任務,難道就係統就開闢 100 個執行緒來分別執行嗎?也不是沒有可能,這取決於你的 CPU,如果在 App 執行時,系統所能承載的最大執行緒個數為 10,那就會開闢這 10 條執行緒來重複執行任務,一次執行 10 個非同步任務。

如果開闢的執行緒上限,那麼剩下的那些任務就暫時無法執行,只能等到前面那些非同步任務的執行緒執行完後,再去執行後面的非同步任務。

總之一句話就是重複利用,先執行完的去執行還沒有開始執行的,如果開闢的執行緒超出限制,那後面的任務就要等待前面的執行緒執行完才能執行。

但是如果開闢很多執行緒的話,會不會對我們的應用程式有負的影響?答案是一定的,開闢一條執行緒就要消耗一定的記憶體空間和系統資源,如果同時存在很多執行緒的話,那本身留給應用程式的記憶體就少得可憐,應用程式在執行時就會很卡,所以並不是執行緒開得越多越好,需要開發者自己平衡。

示例10 - DispatchQueue.global(_:) 全域性併發佇列

除了序列主佇列外,系統還為我們建立了一個全域性的併發佇列 ( DispatchQueue.global() ) ,如果不想自己建立併發佇列,那就用系統的 ( 我們一般也是用系統的 ) 。

DispatchQueue.global().async {
    print("global async start \(Thread.current)")
    DispatchQueue.global().sync {
        (0..<5).forEach {
            print("roop\($0) \(Thread.current)")
            Thread.sleep(forTimeInterval: 0.2)
        }
    }
    print("global async end \(Thread.current)")
}

/**
 global async start <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop0 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop1 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop2 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop3 <NSThread: 0x600002085300>{number = 3, name = (null)}
 roop4 <NSThread: 0x600002085300>{number = 3, name = (null)}
 global async end <NSThread: 0x600002085300>{number = 3, name = (null)}
 */
複製程式碼

和主佇列一樣,它的特殊之處在於,即使是用 sync ,任務也會在其他執行緒執行,至於它在哪一條執行緒執行,我猜測是它一定會讓執行外部 async 的這條執行緒來執行,因為 sync 就是會讓執行緒暫停執行後續操作,等到 sync 執行完後再接著執行,也就是說,在這種情況下,它只能順序執行,那似乎只要一條執行緒就足夠了,沒有必要再開闢新執行緒來執行內部的 sync

另外,全域性併發佇列只有一個,並不是呼叫一次系統就建立一個,經過測試,它們是相等的:

let queue1 = DispatchQueue.global()
let queue2 = DispatchQueue.global()

if queue1 == queue2 { print("相等") }

// Prints "相等"
複製程式碼

總結

在前面的示例中,有關概念都是跟隨示例引申出來的,講得不是那麼統一,在這裡就總結一下。

  • 佇列

    • 序列佇列 在序列佇列中執行任務時,任務按固定順序執行,只能執行完一個任務後,再繼續執行下一個任務 ( 這意味著序列佇列同時只能執行一個任務 ) 。

    • 併發佇列

      併發佇列可以同時執行多個任務,任務並不一定按順序執行,先執行哪幾個任務由系統自動分配決定,等到有某個任務執行完後,就將這個任務出列,然後執行緒才能繼續去執行其他任務。

  • 任務

    • 同步任務

      不管是序列還是非同步佇列,只要是同步任務,就在主執行緒執行 ( DispatchQueue.global().sync 例外 ) 。

      同步任務會阻塞當前執行緒,讓當前執行緒只能等待它執行完畢後才能執行。

      在序列佇列中,任務巢狀了 sync {} 的話會導致死鎖。

    • 非同步任務

      不論是序列還是非同步佇列,只要是非同步任務,就在其他執行緒執行 ( DispatchQueue.main.sync 例外 ) ,不同的是序列佇列在執行非同步任務時,只會開闢一條執行緒,而併發佇列在執行非同步任務時,可以開闢多條執行緒

      非同步任務不會阻塞當前執行緒,執行緒不用等待非同步任務執行完成就可以繼續執行其他任務/操作。

      非同步任務不會產生死鎖。

相關文章