Grand Central Dispatch - Part 4: Dispatch Groups

1. Vấn đề chung khi dùng closure:

Open PhotoManager.swift và check đoạn code ở trong downloadPhotos(withCompletion:):

func downloadPhotos(
    withCompletion completion: BatchPhotoDownloadingCompletionClosure?) {
  var storedError: NSError?
  for address in [PhotoURLString.overlyAttachedGirlfriend,
                  PhotoURLString.successKid,
                  PhotoURLString.lotsOfFaces] {
                    let url = URL(string: address)
                    let photo = DownloadPhoto(url: url!) { _, error in
                      if error != nil {
                        storedError = error
                      }
                    }
                    PhotoManager.shared.addPhoto(photo)
    }
    
    completion?(storedError)
}

The completion closure đã được gửi đến Alert. Cái mình muốn ở đây là gọi closure này khi mà vòng loop của các photos đã downloaded xong. -> Nhầm to! Nó hông rảnh mà đợi cho xong rồi mới gọi cái closure. ^^

Cái bạn muốn là downloadPhotos(withCompletion:) gọi cái completion closure sau khi cái task download các hình đã xong hết. Nhưng đời không như là mơ, ngay sau khi bạn bắt đầu gọi hàm DownloadPhoto(url:) thì nó sẽ trả về ngay lập tức trong khi việc download này vẫn đang diễn ra 1 cách bất đồng bộ. Vì vậy khi completion runs, ko thể đảm bảo được là mấy thằng ảnh kia down xong chưa. Làm sao để theo dõi cái tiến trình bất đồng bộ này, với cơ chế hiện h thì ko biết đc thứ tự, do phụ thuộc nhiều yếu tố.


2. Dispatch Group:

Đây là lý do của dispatch groups. Với dispatch groups, mình có thể group các multiple tasks lại với nhau và hoặc là chờ nó xong hoặc là nhận tín hiệu một khi nó xong. Các task ở đây có thể là bất đồng bộ, đồng bộ hoặc thậm chí run trên những queues khác nhau.

DispatchGroup quản lý dispatch groups. Đầu tiên ta xem xét wait method. Hàm này sẽ blocks thread hiện tại của bạn cho đến khi tất cả các task được sắp xếp thứ tự trong group được finish.

// 1
DispatchQueue.global(qos: .userInitiated).async {
  var storedError: NSError?

  // 2
  let downloadGroup = DispatchGroup()
  for address in [PhotoURLString.overlyAttachedGirlfriend, 
                  PhotoURLString.successKid,
                  PhotoURLString.lotsOfFaces] {
    let url = URL(string: address)

    // 3
    downloadGroup.enter()
    let photo = DownloadPhoto(url: url!) { _, error in
      if error != nil {
        storedError = error
      }   

      // 4
      downloadGroup.leave()
    }   
    PhotoManager.shared.addPhoto(photo)
  }   

  // 5      
  downloadGroup.wait()

  // 6
  DispatchQueue.main.async {
    completion?(storedError)
  }   
}

1. Bởi vì bạn đang dùng 1 hàm wait đồng bộ mà sẽ blocks current thread hiện tại, bạn cần dùng async để đưa toàn bộ method vào background queue để đảm bảo rằng bạn không block main thread.

2. Tạo một new dispatch group.

3. Dùng enter() để thông báo manually đến group rằng task đã bắt đầu. Lưu ý rằng có bao nhiêu enter() thì có bấy nhiều leave(), nếu không là crash á.

4. downloadGroup.leave() để thông báo rằng việc đã xong.

5. Tiếp theo, bạn call hàm wait() để block the current thread để đợi cho tụi nó hoàn thành xong xuôi hết luôn. Ở đây không bị cái zụ chờ miết bởi vì download photo về trong ví dụ ni luôn luôn xong. Lỡ trường hợp mình không biết khi nào xong thì dùng wait(timeout:) để chỉ rõ khi nào timeout và dứt nó ra.

6. Tới lúc ni thì mình đã được đảm bảo là các task hình ảnh đã xong hoặc timeout. Mình có thể make 1 call back đến main queue để run cái completion closure của mình.

Build sẽ thấy nó không có xuất hiện cho đến khi done.

3. DispatchGroup with notify:

Dispatch groups là 1 cái rất hay để handle cho tất cả types of queues. Nên cảnh giác cái số 1, bởi vì khi sử dụng dispatch groups trên main queue thì việc đợi gây ra block UI. Tuy nhiên, sẽ ổn nếu biết kết hợp với mô hình asynchronous để update the UI một khi nhiều long-running tasks finish ví dụ như network calls.

Cách trên hiện giờ thì cũng được, nhưng mà nhìn chung, hạn chế block càng nhiều càng tốt. 

Dispatching asynchronously đến 1 another queue rồi blocking work using wait là hơi ngu. DispatchGroup có chức năng notify you when all the group’s tasks are complete.

// 1
var storedError: NSError?
let downloadGroup = DispatchGroup()
for address in [PhotoURLString.overlyAttachedGirlfriend,
                PhotoURLString.successKid,
                PhotoURLString.lotsOfFaces] {
  let url = URL(string: address)
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url!) { _, error in
    if error != nil {
      storedError = error
    }   
    downloadGroup.leave()
  }   
  PhotoManager.shared.addPhoto(photo)
}   

// 2    
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

Nó work như sau:

1. Ko cần dùng cái method DispatchQueue global nữa, bởi vì mình làm gì có block main thread đâu.

2 notify(queue:work:) đóng vai trò như một asynchronous completion closure. Nó sẽ chạy khi không còn bất kì items nào trong group nữa. Bạn cũng có thể chỉ định là cái công việc completion của bạn là bạn muốn run trên main queue.

Đây là một cách gọn gàng hơn để xử lý công việc cụ thể này vì nó không block any threads.

4. Concurrency Looping

Take a look at downloadPhotos(withCompletion:) in PhotoManager. Trong hàm này có 1 vòng loops và ta xem thử liệu có thể run vòng loop này 1 cách song song để optimize được không.

This is a job for DispatchQueue.concurrentPerform(iterations:execute:). Nó hoạt động y chang vào for nhưng mà nó executes những phần tử bên trong 1 cách song song. Và bản thân hàm này là synchronous và sẽ trả về khi mà mọi công việc đã xong.

var storedError: NSError?
let downloadGroup = DispatchGroup()
let addresses = [PhotoURLString.overlyAttachedGirlfriend,
                 PhotoURLString.successKid,
                 PhotoURLString.lotsOfFaces]
let _ = DispatchQueue.global(qos: .userInitiated)
DispatchQueue.concurrentPerform(iterations: addresses.count) { index in
  let address = addresses[index]
  let url = URL(string: address)
  downloadGroup.enter()
  let photo = DownloadPhoto(url: url!) { _, error in
    if error != nil {
      storedError = error
    }
    downloadGroup.leave()
  }
  PhotoManager.shared.addPhoto(photo)
}
downloadGroup.notify(queue: DispatchQueue.main) {
  completion?(storedError)
}

Bạn phải cẩn thận khi chọn số lần lặp tối ưu cho 1 lượng công việc nhất định. Có nhiều lần lặp quá và mỗi lần lặp chỉ có 1 ít việc trong đó (ý là tác vụ nhanh) có thể tốn nhiều resource trong khi cũng không có lợi gì nhiều. Cái gì ni gọi là striding.


Zậy khi nào thì thích hợp để dùng DispatchQueue.concurrentPerform(iterations:execute:). Loại trừ ra các serial queues. Tốt hơn là dùng cho các concurrent queue mà chưa vòng lặp, đặc biệt là khi cần để track progress.

Thay đoạn loop ở trên bằng để handle DispatchQueue.concurrentPerform(iterations:execute:) to handle concurrent looping.

Có 1 đoạn code lạ là

let _ = DispatchQueue.global(qos: .userInitiated). 

Cái mà tạo ra 1 GCD song song này, thì GCD cần 1 đứa để dựa vào mà biết mà song song á, cho nên ta dùng 1 cái .userInitiated quality of service cho the concurrent calls.

Cơ bản thì đọc cho biết nhưng phần ni hơi thừa, nó tốn nhiều tài nguyên để chạy song song hơn là chỉ run trong vào loop. Chỉ nên dùng đối với 1 mảng quá dài với 1 độ dài thích hợp.

Bình luận
* Các email sẽ không được công bố trên trang web.
I BUILT MY SITE FOR FREE USING