Swift. Grand Central Dispatch (3)

讓我們了解 DispatchGroup 和 DispatchSemaphore

Jeremy Xue
Jeremy Xue ‘s Blog

--

Photo by Martin Adams on Unsplash

▸ 前言:

以下是前兩篇的 GCD 相關文章,建議可以先瀏覽一遍,有些重複的概念在此篇文章不會再次出現。

而這兩個功能常常會一起介紹,功能雖然看似類似,但有著不同的面向,學會這兩個處理方式,對於未來一些多重 API 處理或是共享資源的存取也都會有幫助。

▸ DispatchGroup

透過 DispatchGroup 我們可以管理一組任務並且同步組中的行為。你可以將多個工作項目添加到一個組,並且安排它們在同一個佇列或是不同佇列上異步執行。當所有工作項目完成時,組將會執行其完成處理程序。你也可以同步等待組內的所有任務執行完畢。

任務組完成通知

DispatchWorkItem 相同,我們的 DispatchGroup 也有能夠在任務完成接收通知,並且在指定佇列上執行操作。當我們的 DispatchGroup 中為空(沒有任何任務與 DispatchGroup 相關聯),則會立即執行此 notify 方法的 block

這邊我們建立一個顏色的陣列,並且透過 for-in 的方式異步執行它們,與過去不同的是我們會添加 group 參數,並且添加一個 DispatchGroupnotify 方法來通知我們所有任務皆已完成:

🔴 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 參數來限制等待時間。

手動更新任務組

你也可以透過 enterleave 的方法來手動更新 DispatchGroup 任務的進入和結束。我們移除上面 async 中方法中的 group 參數,接著在 async 之前添加 enter 方法,並且在 asyncblock 中的最後加上 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 方法增加信號量數量,並且透過 waittimeout 的方式來減少信號量數量。

這邊我覺得可以把信號量理解成可用的資源,或許可以幫助理解。因此,當我將初始化一個 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

你可以看到我們的顏色是依照順序被列出的,原因就是我們 DispatchSemaphorevalue 為 1,因此每次只會有一個任務被執行。而每個正在 wait 的任務會等到前一個任務呼叫 signal 後才會開始執行。因此,如果你將這個範例中的 DispatchSemaphorevalue 改為 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 產生。

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

Hi, I’m Jeremy. [好想工作室 — iOS Developer]