Grand Central Dispatch (Part 3)

by Hoang Anh Tuan
824 views

Nội dung bài viết:

  • DispatchBarrier
  • DispatchWorkItem
  • Thread Sanitizer

Dispatch Barrier

  • 1 trong những vấn đề tiêu biểu của đa luồng như đã đề cập ở phần 1 của bài viết là Race Condition.
  • Có 1 cách đơn giản để tránh Race Condition, đó là dùng serial queue để thực hiện các task, khi đó chỉ được 1 task được thực hiện tại mỗi thời điểm, nhưng sẽ làm app chậm lại vì chạy trên serial queue.
  • Vậy nếu bạn muốn các task được thực hiện concurrent, nhưng khi có 1 task thay đổi tài nguyên chung thì chỉ duy nhất task đó được thực hiện tại thời điểm đó?

Đừng lo, GCD cung cấp cho bạn DispatchBarrier để xử lí điều đó 1 cách đơn giản.

Note: Khi 1 task tiến hành việc thay đổi share resources, sẽ có 1 barrier đến, ngăn không cho bất kì 1 task mới nào được thực hiện trong thời gian này. Khi task đó hoàn thành việc thay đổi tài nguyên, barrier mất đi, các tasks tiếp tục chạy concurrent.

Ví dụ sau đây là cách sử dụng Dispatch Barrier để tránh Race Condition.
Giả sử bạn có 1 class Person như sau:

Dưới đây là đoạn code nhiều task khác nhau cùng thay đổi share resources.

let queue = DispatchQueue(label: "01", attributes: .concurrent)
let nameChangeGroup = DispatchGroup()

let person = Person(firstName: "Hoang", lastName: "Tuan")
let nameList = [("A","B"),("C","D"),("E","F"),("G","H"),("I","J")]

for (_, name) in nameList.enumerated() {
    queue.async(group: nameChangeGroup) {
        usleep(UInt32(10000 * idx))
        person.changeName(firstName: name.0, lastName: name.1)
        print("Current name: \(person.name)")
    }
}

nameChangeGroup.notify(queue: .main) {
    print("Final name: \(person.name)")
}

Và đây là kết quả được hiển thị:

Có thể dễ dàng thấy, các task thực hiện thay đổi tài nguyên cùng thời điểm gây ra sai lệch kết quả. Đồng thời, kết quả đều khác nhau sau mỗi lần Run.

Đó là lúc chúng ta sẽ sử dụng Dispatch Barrier để xử lí việc này.

  1. Thực hiện việc get name theo sync để không cho các task khác có thể đọc tài nguyên khi có 1 task đang đọc, vì có thể task đang đọc có thể sẽ chỉnh sửa tài nguyên sau đó.
  2. Đưa toàn bộ thân hàm của func changeName vào thực hiện trên luồng isolationQueue1 với flags là 1 barrier. Vì vậy, khi excute changeName, 1 barrier sẽ xuất hiện và không để bất kì task mới nào được thực hiện cho đến khi changeName hoàn thành.

Và đây là kết quả:

Kết quả khi đó được thực hiện đúng

DispatchWorkItem

DispatchWorkItem là những block of code. Điều khác biệt khi sử dụng DispatchWorkItem là bạn có thể cancel những task đang trong queue.

Note: Bạn chỉ có thể cancel 1 DispatchWorkItem trước khi nó đi đến đầu queue và bắt đầu execute.

Khởi tạo 1 DispatchWorkItem:

let workItem = DispatchWorkItem(qos: .background, flags: .inheritQoS) {
     guard let url = URL(string: self.links[i]) else {
         return
     }
     URLSession.shared.dataTask(with: url) { (data, res, err) in
         guard let imageData = data else {
             return
         }
         let myImage = UIImage(data: imageData)
         DispatchQueue.main.async {
             self.listImage[i].image = myImage
         }
     }.resume()
}

Đưa DispatchWorkItem vào queue để thực hiện:

DispatchQueue.global().async(execute: workItem)

Cancel 1 DispatchWorkItem:

workItem.cancel()

Thread Sanitizer

Sử dụng Thread Sanitizer để phát hiện & debug race condition:

  • Chọn Product>Scheme>Edit Scheme
  • Chọn vào "Thread Sanitizer":

Khi đó, nếu code của bạn bị race condition, xcode sẽ thông báo cho bạn:

1 tool khá hay để phát hiện race condition

Kết luận: GCD là 1 API đỉnh và dễ sử dụng để quản lí multitask, đa luồng, nhưng có 1 nhược điểm là không cung cấp các state của task trong trường hợp bạn muốn quản lí sâu hơn. Khi đó, bạn phải tự custom State cho riêng mình, hoặc dùng Operation.

-End-

Leave a Comment

* By using this form you agree with the storage and handling of your data by this website.