DispatchGroup 與 DispatchWorkItem

GCD(Grand Central Dispatch)裡面的兩種方法介紹:

  1. DispatchGroup: DispatchGroup 是一種用於追蹤一組非同步任務(通常是使用 GCD 創建的)的進度的機制。它可以幫助你確定所有的任務是否已經完成,從而在所有任務都完成後執行特定的程式碼。你可以使用 enter() 方法來追蹤進入群組的任務數量,使用 leave() 方法來標記任務已完成,然後可以使用 notify(queue:execute:) 方法在群組中的所有任務都完成後執行指定的程式碼。
  2. DispatchWorkItem: DispatchWorkItem 是一個輕量級的任務(closure),可以使用 GCD 運行它,也可以在其他地方運行。它不僅允許你定義你的任務,還可以控制任務的取消、延遲執行等。

DispatchGroup 用於管理一組非同步任務的進度,而 DispatchWorkItem 則用於創建和控制單個任務。這兩個類別結合使用可以有效地處理多個非同步任務,並在它們完成時執行相應的程式碼。

使用DispatchGroup 一次請求多次API,要特別注意的是 enter 的次數要跟 leave 的次數是相等的,

func randomBreedImages(completion: @escaping ([[String: URL]]) -> Void) {
var breeds: [[String: URL]] = []
//創建 DispatchGroup 同步多個非同步任務
let dispatchGroup = DispatchGroup()
//進入一個任務
dispatchGroup.enter()
dispatchGroup.enter()

Network.getBreedList { breedList in
if let breed = breedList.randomElement() {
do {
try Network.getImageUrl(breed: breed) { url in

if let url {
breeds.append([breed : url])
print("breeds1:\(breeds)")
//加入完畢就直接離開任務
dispatchGroup.leave()
}
}
} catch {
print("Error fetching image for breed \(breed): \(error)")
//如果網路請求失敗就進去 catch 一樣直接離開任務
dispatchGroup.leave()
}


Network.getSubBreedList(breed) { subList in
for subBreed in subList {
let combinedBreed = breed + " " + subBreed
//這邊只要重複就不在請求網路
if !breeds.contains { $0.keys.first == combinedBreed } {
//在 for 裡面創建多個任務進入點
dispatchGroup.enter()
do {
try Network.getImageUrl(breed: breed, subBreed: subBreed) { url in
if let url {
breeds.append([breed + " " + subBreed : url ])
print("breeds2:\(breeds)")
//每請求完畢就離開任務
dispatchGroup.leave()
}
}
} catch {
print("Error fetching image for breed \(breed) and subBreed \(subBreed): \(error)")
dispatchGroup.leave()
}
}
}
}
}
}
dispatchGroup.leave()
dispatchGroup.wait()
//等待都請求完畢後在收集全部的任務後執行
dispatchGroup.notify(queue: .main) {
completion(breeds)
print("main \(breeds.count)")

}
}

這邊使用DispatchWorkItem來達到使用搜尋匡每輸入一個就自動向 API 請求一次,$0.lowercased() 是將品種名稱轉換成小寫字串,以便進行比對,請求完成的會 reloadData 讓畫面更新,為了防止大量請求而使用了 searchWorkItem?.cancel 與 [weak self] 都與處理多線程編程(concurrent programming)和避免引起強引用循環(retain cycle)有關。

    var searchWorkItem: DispatchWorkItem?
let dispatchQueue = DispatchQueue(label: "searchQueue", qos: .userInitiated)


func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
var searchContent: [[String: URL]] = []

searchWorkItem?.cancel()

if searchText.isEmpty {
//進入 seachBar 後 讓一起同步存取值的變數,賦予給主要的變數 breedContent
DispatchQueue.main.async {
self.breedContent = self.content
self.collectionView.reloadData()
}
}else{
searchWorkItem = DispatchWorkItem { [weak self] in
Network.getBreedList { breedList in
//利用 searchText 來過濾 寵物的列表
let filtered = breedList.filter { $0.lowercased().contains(searchText.lowercased()) }
for breedName in filtered {
// 取得到過濾後的列表向網路請求圖片
Network.getImageUrl(breed: breedName) { url in
if let url {
// 利用一個專門存取 search 結果的變數
searchContent.append([breedName : url])
DispatchQueue.main.async {
self?.breedContent = []
self?.breedContent = searchContent
self?.collectionView.reloadData()
}
}
}
}
}
}
}
dispatchQueue.asyncAfter(deadline: .now() + 0.5, execute: searchWorkItem!)
}

GPT補充:

  1. DispatchWorkItem.cancelDispatchWorkItem 是 GCD(Grand Central Dispatch)中的一個類別,用於表示要在某個佇列上執行的工作(task)。當你創建一個 DispatchWorkItem 並將其提交到一個佇列上時,你可以稍後使用 cancel 方法來取消這個工作的執行。這在某些情況下可能很有用,例如,如果你在主線程上執行了一個耗時的工作,但在某個點上你希望取消它,你可以調用 cancel 方法。這可以幫助你更好地控制和管理佇列中的工作。
  2. [weak self]: 在 Swift 中,當你在一個 closure 內部捕獲一個對 self 的引用時,可能會導致強引用循環。這種情況在當一個對象(通常是類實例)持有一個 closure,而 closure 同時又捕獲了這個對象的引用時發生。為了避免這種強引用循環,常見的方法是在 closure 內部使用 [weak self][unowned self] 來捕獲對 self 的弱引用或無主引用。
  3. [weak self] 的主要目的是避免強引用循環。當你在 closure 內部使用 [weak self] 時,即使在 closure 中持有了對 self 的引用,它也不會增加 self 的引用計數。如果在後續的執行過程中,self 已經被釋放,則使用弱引用 [weak self] 可以確保不會對已釋放的對象進行操作。

總結起來,DispatchWorkItem.cancel 用於取消工作項目的執行,而 [weak self] 用於避免在 closure 中引起強引用循環,從而避免內存泄漏。在處理多線程編程和保護對象生命週期方面,這兩者都是有用的工具。

--

--