Swift. Grand Central Dispatch (2)

讓我們看看如何使用 DispatchWorkItem

Jeremy Xue
Jeremy Xue ‘s Blog

--

Photo by sebastiaan stam on Unsplash

▸ 前言:

我們延續上一篇文章,接著介紹更多關於 GCD 的相關用法:

https://medium.com/jeremy-xue-s-blog/swift-grand-central-dispatch-1-7db1ab292b3c

▸ DispatchWorkItem

DispatchWorkItem 封裝了要在 DispatchQueueDispatchGroup 內執行的工作。 您還可以將它用作 DispatchSource 事件、註冊或取消處理程序。

而其中初始化 DispatchWorkItem 我們可以提供以下參數:

  • qos
    優先考慮工作項目的執行時要使用的服務品質。預設為 .unspecified
  • flag
    工作項目的配置旗幟。
  • block
    執行工作的塊。

將返回的 DispatchWorkItem 提交到佇列來安排它在佇列中執行。DispatchQueue 可以根據佇列底層執行緒的 flagsqos 來更改指定的服務品質等級。但是,佇列永遠不會執行服務品質等級低於 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

我們也可以透過 DispatchQueueasyncsync 方法來執行工作項目:

🟡 thread: <NSThread: 0x600000840dc0>{number = 1, name = main}
🔴 thread: <NSThread: 0x60000084a880>{number = 3, name = (null)}
🟡 start
🔴 start
🟡 1
🟡 2
🟡 3
🟡 end
🔴 1
🔴 2
🔴 3
🔴 end

而關於 DispatchWorkItemqos 參數,上面也有提到說「佇列永遠不會執行服務品質等級低於 qos 參數的 block」。因此,當佇列的 qos 高於工作項目的 qos 時,會以佇列的 qos 為主來執行。

首先我們先添加這個輔助函數來幫我們查詢目前的 QoS:

接著讓我們將 DispatchWorkItemqos 指定為 .background,而 global queue 的 qos 指定為 .userInteractive,並且查看結果:

Current QoS: userInteractive

可以看見結果為 userInteractive,符合我們的理解,接下來讓我們將兩者的 qos 調換,並且再次查看結果:

Current QoS: background

而我們也可以透過 DispatchWorkItemflags 參數來指定 qos 處理方式:

.noQoS 。不指定 QoS,結果應該與上面的結果相同。

Current QoS: userInteractive

.inheritQos 。首選與當前執行上下文關聯的服務品質。

Current Qos: background

.enforceQoS。首選與 block 相關聯的服務品質,因此會是 DispatchWorkItemqos

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 這些任務是併發且異步執行的,因此印出的結果和結果不固定。這時,我們在 item2flags 中添加 .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 方法也可以提供 qosflags 或是以 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

--

--

Jeremy Xue
Jeremy Xue ‘s Blog

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