Swift. Grand Central Dispatch (3)
讓我們了解 DispatchGroup 和 DispatchSemaphore
▸ 前言:
以下是前兩篇的 GCD 相關文章,建議可以先瀏覽一遍,有些重複的概念在此篇文章不會再次出現。
而這兩個功能常常會一起介紹,功能雖然看似類似,但有著不同的面向,學會這兩個處理方式,對於未來一些多重 API 處理或是共享資源的存取也都會有幫助。
▸ DispatchGroup
透過 DispatchGroup 我們可以管理一組任務並且同步組中的行為。你可以將多個工作項目添加到一個組,並且安排它們在同一個佇列或是不同佇列上異步執行。當所有工作項目完成時,組將會執行其完成處理程序。你也可以同步等待組內的所有任務執行完畢。
任務組完成通知
與 DispatchWorkItem
相同,我們的 DispatchGroup
也有能夠在任務完成接收通知,並且在指定佇列上執行操作。當我們的 DispatchGroup
中為空(沒有任何任務與 DispatchGroup
相關聯),則會立即執行此 notify
方法的 block
:
這邊我們建立一個顏色的陣列,並且透過 for-in 的方式異步執行它們,與過去不同的是我們會添加 group
參數,並且添加一個 DispatchGroup
的 notify
方法來通知我們所有任務皆已完成:
🔴 thread: <NSThread: 0x60000128bdc0>{number = 6, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟢 thread: <NSThread: 0x600001299500>{number = 5, name = (null)}
🟢 start
🟢 1
🟢 2
🟡 thread: <NSThread: 0x600001280480>{number = 7, name = (null)}
🟡 start
🟢 3
🟢 end
🟡 1
🟡 2
🟡 3
🟡 end
🚩 Finish!!
你也可以試著在 async
中加入 sleep
讓我們的程式睡覺(凍結)一會兒,可能可以更容易看出結果:
🔴 thread: <NSThread: 0x600000b69cc0>{number = 6, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x600000b69cc0>{number = 6, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🟢 thread: <NSThread: 0x600000b69cc0>{number = 6, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
🚩 Finish!!
等待任務組
DispatchGroup
除了有 DispatchWorkItem
相同功能的 notify
,這邊我們也可以使用 wait
方法來等待任務組。
🟡 thread: <NSThread: 0x600002b312c0>{number = 5, name = (null)}
🟡 start
🔴 thread: <NSThread: 0x600002b49000>{number = 7, name = (null)}
🔴 start
🟢 thread: <NSThread: 0x600002b84040>{number = 6, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
🔴 1
🔴 2
🟡 1
🟡 2
🟡 3
🔴 3
🔴 end
🟡 end
🚩 Finish!!
當然,也可以加上 timeout
參數來限制等待時間。
手動更新任務組
你也可以透過 enter
和 leave
的方法來手動更新 DispatchGroup
任務的進入和結束。我們移除上面 async
中方法中的 group
參數,接著在 async
之前添加 enter
方法,並且在 async
的 block
中的最後加上 leave
。你應該會得到與上述相同的操作結果。
🔴 thread: <NSThread: 0x6000033e6c00>{number = 4, name = (null)}
🔴 start
🟡 thread: <NSThread: 0x6000033c3340>{number = 5, name = (null)}
🟡 start
🟢 thread: <NSThread: 0x6000033b9280>{number = 7, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
🟡 1
🔴 1
🟡 2
🟡 3
🟡 end
🔴 2
🔴 3
🔴 end
🚩 Finish!!
用手動處理可以靈活的調整任務的開始和結束,概念上也很容易理解,當所有 enter
的工作項目 leave
後,那麼表示 group 任務完成,但在使用上這兩者必須組合使用。如果嘗試 enter
但從未 leave
那麼永遠都不會收到 group 的完成通知,而如果你嘗試 leave
但未 enter
的話,那麼可能導致你的應用崩潰。
▸ DispatchSemaphore
DispatchSemaphore
是傳統計數信號量的實現。只有當呼叫執行緒需要被阻擋時,DispatchSemaphore
才會調用內核。如果呼叫的信號量不需要阻擋,則不進行內核呼叫。
你可以透過調用 signal
方法增加信號量數量,並且透過 wait
或 timeout
的方式來減少信號量數量。
這邊我覺得可以把信號量理解成可用的資源,或許可以幫助理解。因此,當我將初始化一個 DispatchSemaphore 並且 value 設置為 1,你也可以理解為「我的資源只有一個,因此每次只能有一個人使用,其他人必須等待」。
首先,我們使用與上面 DispatchGroup
相同的範例來查看原本的執行結果:
🔴 thread: <NSThread: 0x6000014c8780>{number = 6, name = (null)}
🟢 thread: <NSThread: 0x6000014cc140>{number = 4, name = (null)}
🔴 start
🟢 start
🟡 thread: <NSThread: 0x600001499e00>{number = 7, name = (null)}
🟡 start
🟢 1
🟢 2
🟢 3
🟢 end
🔴 1
🔴 2
🔴 3
🔴 end
🟡 1
🟡 2
🟡 3
🟡 end
接著,我們建立一個 DispatchSemaphore
並且將 value
設置為 1 來限制每次只允許一個資源能夠被使用,在還沒開始前使用 wait
來減少信號量(等待資源),並且在執行完成之後透過 singal
來增加信號量(釋放資源):
🔴 thread: <NSThread: 0x6000018a04c0>{number = 3, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x6000018a04c0>{number = 3, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🟢 thread: <NSThread: 0x6000018a04c0>{number = 3, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
你可以看到我們的顏色是依照順序被列出的,原因就是我們 DispatchSemaphore
的 value
為 1,因此每次只會有一個任務被執行。而每個正在 wait
的任務會等到前一個任務呼叫 signal
後才會開始執行。因此,如果你將這個範例中的 DispatchSemaphore
的 value
改為 3
,執行結果會與沒有使用 DispatchSemaphore
的狀況相同。
你也可以將 async
改成 afterAfter
來進行延遲執行的操作或是透過 sleep
暫時凍結應用,也都會得到相同結果。
🔴 thread: <NSThread: 0x600000ef2b80>{number = 6, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x600000ef2b80>{number = 6, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🟢 thread: <NSThread: 0x600000ef2b80>{number = 6, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
當你使用 wait
時,會減少信號量。如果信號量 < 0,執行緒會被阻擋;如果信號量 ≥ 0,則不需要等待。
而使用 signal
時,會增加信號量。如果信號量 < 0,則恢復佇列中等待的最舊的執行緒;如果信號量 ≥ 0,則表示目前佇列為空,沒有人在等待。
因此,以我上面可用的資源量來解釋。wait 時,會看看目前是否有資源,有就執行,沒有就繼續等待。而 signal 則是表示有資源被釋放,可以讓正在 wait 的人使用。
而我們的 wait
方法也可以加上 timeout
參數來限制等待資源的時間。
首先,我們在 wait
中添加 timeout
參數,並且在 async
方法中添加 sleep
來暫時凍結應用程序,接著讓我們查看結果:
🔴 thread: <NSThread: 0x6000015042c0>{number = 7, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 timeout
🟢 timeout
由結果可以看出,🔴 因為不需要等待,所以會直接執行,而其他兩個顏色的任務因為 sleep
等待的時間過長導致 timeout 產生。