iOS - 多線程分析之 DispatchQueue Ⅰ

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

DispatchQueue ( 調度隊列 ) 就是被定義在 Dispatch 框架中,能夠用來執行跟多線程有關操做的類。swift

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

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

同步和異步執行

同步和異步執行

如圖。同步和異步的區別在於,線程會等待同步任務執行完成;線程不會等待異步任務執行完成,就會繼續執行其餘任務/操做。app

閱讀指南:框架

本文中出現的 "任務" 是指 sync {}async {} 中整個代碼塊的統稱,"操做" 則是在 "任務" 中執行的每一條指令 ( 代碼 ) ;由於主線程沒有 "任務" 之說,主線程上執行的每一條 ( 段 ) 代碼,都統稱爲 "操做"。異步

串行和併發隊列

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

串行 (Serial) 隊列中執行任務時,任務會按照固定順序執行,執行完一個任務後再繼續執行下一個任務 (這意味着串行隊列同時只能執行一個任務) ;在併發 (Concurrent) 隊列中執行任務時,任務能夠同時執行 ( 實際上是在以極短的時間內不斷的切換線程執行任務 ) 。oop

串行和併發隊列都以 先進先出 (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 例外 ) ,不一樣的是串行隊列在執行異步任務時,只會開闢一條線程,而併發隊列在執行異步任務時,能夠開闢多條線程

      異步任務不會阻塞當前線程,線程不用等待異步任務執行完成就能夠繼續執行其餘任務/操做。

      異步任務不會產生死鎖。

相關文章
相關標籤/搜索