iOS Concurrency - Phần 3.4: Grand Central Dispatch

Dispatch Group

Vấn đề

Giả sử chúng ta có một View Controller có chứa một imageView. ImageView này load một hình ảnh từ Internet về. Chúng ta muốn rằng sau khi image được download từ Internet về (//1), nó sẽ được hiển thị lên ImageView (//2) và sau đó một alert được hiển thị để báo rằng ImageView download thành công (//3).

class ViewController: UIViewController {
  @IBOutlet weak var img: UIImageView!
  override func viewDidLoad() {
    super.viewDidLoad()
    let urlString = "http://wallpapershome.com/images/pages/pic_hs/10151.jpg"
    DispatchQueue.global(qos: .userInitiated).async {
      let data =  NSData(contentsOf: URL(string: urlString)!) //1
      DispatchQueue.main.async {
        self.img.image = UIImage(data: Data(_:data as! Data)) //2
      }
    }
    displayAlert() //3
  }
  
  func displayAlert(){
    let alert = UIAlertController(title: "Download", message: "The image downloads successful.”, preferredStyle: .alert)
    alert.addAction(UIAlertAction(title: "OK", style: .cancel, handler: nil))
    self.present(alert, animated: true, completion: nil)
  }
}

Khi chạy đoạn code trên bạn sẽ gặp phải trường hợp rằng Alert sẽ được display trước khi hình ảnh được load từ internet về để hiển thị lên ImageView. Đây không phải là thứ mà chúng ta mong đợi. Data của hình ảnh được download ở đoạn code (//1), lời gọi này được trả về ngay lập tức nhưng thật sự là hình ảnh vẫn được download async. Do đó khi alert hiển thị thì chưa chắc gì hình ảnh được download xong. Những gì chúng ta mong đợi là alert sẽ được display sau khi image được load xong. Do đó, chúng ta cần giám sát để biết được khi nào task load hình được hoàn thành. Apple cung cấp cho chúng ta GroupDispatch để handle problem này.

Giải pháp 1

Dipatch Group cho phép chúng ta nhóm nhiều task với nhau và đợi cho chúng được hoàn thành hay được thông báo khi chúng được hoàn tất. Những task này có thể sync hay async và có thể chạy trên nhiều queue khác nhau. Dispatch Group cung cấp method wait mà nó block thread hiện tại cho tới khi tất cả các task trong group hoàn thành. Chúng ta sẽ thay thế đoạn code trên thành đoạn code dưới đây:

let urlString = "http://wallpapershome.com/images/pages/pic_hs/10151.jpg"
DispatchQueue.global(qos: .userInitiated).async { //4
  let downloadGroup = DispatchGroup() //5
  downloadGroup.enter() //6
  let data =  NSData(contentsOf: URL(string: urlString)!)
  DispatchQueue.main.async {
    self.img.image = UIImage(data: Data(_:data as! Data))
  }
  downloadGroup.leave() //7
  downloadGroup.wait() //8
  DispatchQueue.main.async { //9
    self.displayAlert()
  }
}

(//4) : Bởi vì chúng ta sử dụng method đồng bộ wait mà nó block queue hiện tại —> chúng ta cần sử dụng async để đưa toàn bộ method xuống background global queue nhằm đảm bảo không khoá main thread. (//5) : Chúng ta tạo mới một DispatchGroup (//6): Chúng ta gọi method enter() để báo với group rằng một task đã được bắt đầu (//7): Thông báo với group rằng task đã hoàn thành xong Chúng ta cần đảm bảo rằng số lượng lời gọi enter phải bằng với số lượng lời gọi leave để không bị crash app. (//8): Chúng ta gọi wait() nhằm block luồng hiện tại và đợi cho tất cả công việc trước đó trong group hoàn thành. Chúng ta có thể sử dụng wait(timeout:) để đặc tả thời gian timeout nhằm tránh việc đợi quá lâu (//9): Tại thời điểm này, tất cả hình ảnh đã được load xong hoặc timeout. Alert hiển thị nhằm thông báo hình ảnh được load xong.

Giải pháp 2

Việc dispatch async tới một queue khác sau đó block công việc sử dụng wait chưa hẳn là giải pháp tối ưu. Sau đây mình sẽ trình bày thêm một cách khác: chúng ta sẽ sử dụng dispatchgroup để thông báo (notify) khi nào công việc của một group hoàn thành. Chúng ta chỉnh sửa lại đoạn code trên như sau:

//8
let urlString = "http://wallpapershome.com/images/pages/pic_hs/10151.jpg"
let downloadGroup = DispatchGroup()
downloadGroup.enter()
let data =  NSData(contentsOf: URL(string: urlString)!)
self.img.image = UIImage(data: Data(_:data as! Data))
downloadGroup.leave()

downloadGroup.notify(queue: DispatchQueue.main) { //9
  self.displayAlert()
}

(//8) trong cách hiện thực mới này chúng ta không cần bao method trong lời gọi async bởi vì chúng ta không sử dụng wait để block main thread (//9) DisparchGroup sẽ thông báo với chúng ta biết khi không còn một task nào trong group để chạy công việc chúng ta mong muốn trên queue được đặc tả.

Những gì mình sẽ nói tiếp?

Thông qua bài giới thiệu nho nhỏ này, các bạn chắc hẳn đã biết sử dụng DispatchGroup để kiểm soát và gọi các task ở những thời điểm thích hợp. Ở bài tiếp theo mình sẽ giới thiệu Dispatch Work Item và một số kỹ thuật liên quan tới nó. Hẹn gặp các bạn ở những bài viết kế tiếp trong chuỗi series về iOS concurrency.

Tham khảo

https://www.raywenderlich.com/148515/grand-central-dispatch-tutorial-swift-3-part-2