Swift. Grand Central Dispatch (2)
讓我們看看如何使用 DispatchWorkItem
▸ 前言:
我們延續上一篇文章,接著介紹更多關於 GCD 的相關用法:
https://medium.com/jeremy-xue-s-blog/swift-grand-central-dispatch-1-7db1ab292b3c
▸ DispatchWorkItem
DispatchWorkItem
封裝了要在 DispatchQueue
或 DispatchGroup
內執行的工作。 您還可以將它用作 DispatchSource
事件、註冊或取消處理程序。
而其中初始化 DispatchWorkItem
我們可以提供以下參數:
qos
優先考慮工作項目的執行時要使用的服務品質。預設為.unspecified
。flag
工作項目的配置旗幟。block
執行工作的塊。
將返回的 DispatchWorkItem
提交到佇列來安排它在佇列中執行。DispatchQueue
可以根據佇列底層執行緒的 flags
和 qos
來更改指定的服務品質等級。但是,佇列永遠不會執行服務品質等級低於 qos
參數的 block
。
執行 DispatchWorkItem
你還可以透過調用其 perform()
方法直接在當前上下文中執行 DispatchWorkItem
。當直接執行工作項目時,系統永遠不會執行服務品質等級低於 qos
參數的 block
。
讓我們試著透過 perform
來執行我們的 DispatchWorkItem
:
🔴 thread: <NSThread: 0x600002e881c0>{number = 1, name = main}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
但要記得 perform()
是在當前執行緒上同步執行工作項目:
🔴 thread: <NSThread: 0x6000039dc3c0>{number = 1, name = main}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x6000039dc3c0>{number = 1, name = main}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🟢 thread: <NSThread: 0x6000039dc3c0>{number = 1, name = main}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
我們也可以透過 DispatchQueue
的 async
或 sync
方法來執行工作項目:
🟡 thread: <NSThread: 0x600000840dc0>{number = 1, name = main}
🔴 thread: <NSThread: 0x60000084a880>{number = 3, name = (null)}
🟡 start
🔴 start
🟡 1
🟡 2
🟡 3
🟡 end
🔴 1
🔴 2
🔴 3
🔴 end
而關於 DispatchWorkItem
的 qos
參數,上面也有提到說「佇列永遠不會執行服務品質等級低於 qos
參數的 block
」。因此,當佇列的 qos
高於工作項目的 qos
時,會以佇列的 qos
為主來執行。
首先我們先添加這個輔助函數來幫我們查詢目前的 QoS:
接著讓我們將 DispatchWorkItem
的 qos
指定為 .background
,而 global queue 的 qos
指定為 .userInteractive
,並且查看結果:
Current QoS: userInteractive
可以看見結果為 userInteractive
,符合我們的理解,接下來讓我們將兩者的 qos
調換,並且再次查看結果:
Current QoS: background
而我們也可以透過 DispatchWorkItem
的 flags
參數來指定 qos 處理方式:
.noQoS
。不指定 QoS,結果應該與上面的結果相同。
Current QoS: userInteractive
.inheritQos
。首選與當前執行上下文關聯的服務品質。
Current Qos: background
.enforceQoS
。首選與 block
相關聯的服務品質,因此會是 DispatchWorkItem
的 qos
:
Current QoS: userInteractive
而我們剩下還有三種不同的 flags
:
assignCurrentContext
設置此旗幟後,工作項目從負責執行任務的 DispatchQueue 或 Thread 繼承像是服務品質相關的屬性。barrier
當提交至 concurrent queue 時,帶有此旗幟的工作項目作為屏障。在屏障之前提供的工作執行完成,此屏障的工作項目將會執行。一旦屏障工作項目完成,佇列將安排在屏障之後的工作項目。detached
設置此旗幟後,系統不會將當前執行上下文中的屬性應用至工作項目。
由於 .barrier 這個 flag 比較常使用,而其他兩者的 flags 找到的資訊比較少,自己測試上也看不太出差異,因此未來了解後再補上。(如果了解用途的讀者也歡迎與我分享)
而 .barrier
是一個比較常用的 flag,其效用就如他的名稱相同 — 「屏障」,我們可以從下面的程式碼看出差異。首先,我們先看看還沒添加 flags 前 concurrent queue 的結果:
🟡 thread: <NSThread: 0x60000254cb40>{number = 6, name = (null)}
🟢 thread: <NSThread: 0x600002555140>{number = 5, name = (null)}
🟢 start
🔴 thread: <NSThread: 0x60000256e6c0>{number = 7, name = (null)}
🔴 start
🟡 start
🟢 1
🟢 2
🟢 3
🟢 end
🔴 1
🔴 2
🔴 3
🔴 end
🟡 1
🟡 2
🟡 3
🟡 end
因為是 concurrent queue 這些任務是併發且異步執行的,因此印出的結果和結果不固定。這時,我們在 item2
的 flags
中添加 .barrier
,再嘗試運行一次:
🔴 thread: <NSThread: 0x600003090100>{number = 3, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x600003090100>{number = 3, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🟢 thread: <NSThread: 0x600003090100>{number = 3, name = (null)}
🟢 start
🟢 1
🟢 2
🟢 3
🟢 end
可以看到 🟡 工作項目會在 🔴 執行結束後才會執行,並且 🟢 的工作項目,會等到 🟡 結束後才開始執行。有點像是暫時阻擋工作項目的執行,直到標記為 .barrier
的任務完成。
DispatchQueue 的 async 和 sync 方法也可以設置 qos 與 flags。
任務完成通知
而我們可以透過 notify()
的方法來在工作項目完成後,執行更多操作,同時也能夠指定在特定的佇列上執行:
🔴 thread: <NSThread: 0x600003d02c00>{number = 9, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x600003d68d40>{number = 1, name = main}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
可以看到我們透過 notify
方法可以在 item1
執行結束後在 main queue 上執行 item2
。當然這個 notify 方法也可以提供 qos
、flags
或是以 block
方式使用:
等待任務
我們還可以透過 wait()
方法來等待某個工作項目完成,而 wait 的這個方法是同步的,因此他會等到該工作項目完成後才會接續處理。
waiting for 🔴
🔴 thread: <NSThread: 0x600002ae4300>{number = 5, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
🟡 thread: <NSThread: 0x600002ae4300>{number = 5, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
由結果可以看見我們會等待 🔴 的工作項目在一秒的延遲後執行完成,我們的 🟡 才會接續執行。
當然我們的 wait
的方法也可以設置 timeout
的時間,可以讓你在等待超時的狀況發生後進行其他的處理。
waiting for 🔴
🔴 time out
🟡 thread: <NSThread: 0x600003760f40>{number = 4, name = (null)}
🟡 start
🟡 1
🟡 2
🟡 3
🟡 end
🔴 thread: <NSThread: 0x600003760f40>{number = 4, name = (null)}
🔴 start
🔴 1
🔴 2
🔴 3
🔴 end
但發生 timeout 後,該工作項目還是會繼續執行(不會停止),因此,從結果可以看到我們等待 🔴 並且發生等待超時的狀況,但在五秒的延遲過後,🔴 還是會繼續執行。
取消任務
你也可以透過 cancel()
來異步取消工作項目(但對於開始的工作項目不受影響),然後可以透過 isCancelled
來得知任務是否被取消。以下這段程式碼不會印出任何內容:
🔴 false
🔴 true