+5

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


All rights reserved

Bình luận

Đăng nhập để bình luận
Avatar

Ở cách 1, m có thể để self.displayAlert() ngay dưới dòng code: self.img.image = UIImage(data: Data(_:data as! Data)) và nó vẫn chạy bình thường, không biết cách này có vấn đề gì ko. Ở cách 2, cho m hỏi: DispatchGroup là chạy trên 1 thread khác (ko phải main thread) đúng ko.

Avatar

Câu hỏi thứ 1: Bởi vì bạn để self.displayAlert() ngay dưới dòng self.img.image = UIImage(data: Data(_:data as! Data)) nên nó nằm trong main queue. Mà bản chất của main queue chính là serial queue (chạy tuần tự), do đó kết quả vẫn đúng. Ở đây mình đang demo cách sử dụng DispatchGroup nên mình mới sắp xếp vị trí như vậy. Câu hỏi thứ 2 : bạn có thể chạy DispatchGroup trên bất cứ thread nào do bạn chỉ định.

Avatar

ok, cám ơn bạn.

Avatar
@ankb9x
thg 1 24, 2022 5:03 SA

@tuananhsteven Mình đã test thử cả giải pháp 1 và giải pháp 2 thì đang thấy:

  1. Giải pháp 1: Dù có bỏ hay thêm câu lệnh "downloadGroup.wait() " ở step8 thì việc display alert cũng luôn hiển thị sau câu lệnh show image

Screen Shot 2022-01-24 at 12.08.33.png

Vấn đề nằm ở chỗ câu lệnh: let data = NSData(contentsOf: URL(string: urlString)!) đã block thread hiện tại rồi nên chắc chắn data của image phải được load về hết thì các step 6*, 7, 8, 9 mới được chạy.

Ví dụ này nên điều chỉnh thành như sau sẽ dễ hình dung hơn:

Screen Shot 2022-01-24 at 12.05.49.png

  • Load image data được thực hiện trên 1 thread riêng
  • Lúc này việc có wait ở step 8 mới có ý nghĩa.
  1. Giải pháp 2:
  • Việc load data image ở giải pháp 2 phải được đặt vào 1 thread khác, ko sẽ bị block thread hiện tại tương tự như giải pháp 1.
  • Câu lệnh self.displayAlert() luôn được gọi sau khi load image data let data = NSData(contentsOf: URL(string: urlString)!) vì câu lệnh trên đã block thread và wait cho đến khi data load xong hoàn toàn nên việc đặt self.displayAlert() trong notify của DispatchGroup không còn ý nghĩa.

Ví dụ này nên điều chỉnh thành như sau sẽ dễ hình dung hơn:

Screen Shot 2022-01-24 at 12.05.30.png

Avatar
+5
Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí