Contents
Chào bạn, chúng ta lại tiếp tục nổ não với chủ đề “Grand Central Dispatch” trong bài viết này. Nếu bạn chưa đọc qua về nó thì có thể ghé link sau để bổ túc thêm kiến thức cho mình:
Còn bây giờ thì bắt đầu thôi!
Chuẩn bị
- MacOS 10.14.4
- Xcode 11.0
- Swift 5.1
1. Concurrent Queues
Các ví dụ ở phần đầu tiên thì nếu bạn tinh ý thì sẽ thấy 1 điều. Tất cả chúng đều là serial queue. Vậy tụi nó như thế nào?
- Một queue chỉ thực hiện 1 công việc
- Các queue được thực hiện theo thứ tự được thêm vào
- Dựa vào
QoS
và độ ưu tiên để tạo nên sự hoạt động bất đồng bộ giữa các queue khác nhau
Phần 1 giới thiệu cho chúng ta về các Queue và cách hoạt động giữa các Queue khác nhau thì sẽ như thế nào. Bây giờ, thì có một vấn đề như sau.
- Với 1 queue thì đảm bảo các công việc được thêm vào queue đó sẽ hoạt động một cách đồng bộ
- Không dùng nhiều queue
- Hạn chế tài nguyên hệ thống cấp pháp
Giải pháp: sử dụng Concurrent Queue để thực hiện nhiều công việc một cách đồng thời trong 1 queue.
Ví dụ với Serial Queue
let queue = DispatchQueue(label: "com.fx.myqueue", qos: .utility) queue.async { for i in 0..<10 { print("🔴", i) } } queue.async { for i in 100..<110 { print("🔵", i) } } queue.async { for i in 1000..<1010 { print("⚫️", i) } }
Kết quả
- Các màu sắc in ra theo đúng thứ tự thực thi
🔴 0 🔴 1 🔴 2 🔴 3 🔴 4 🔴 5 🔴 6 🔴 7 🔴 8 🔴 9 🔵 100 🔵 101 🔵 102 🔵 103 🔵 104 🔵 105 🔵 106 🔵 107 🔵 108 🔵 109 ⚫️ 1000 ⚫️ 1001 ⚫️ 1002 ⚫️ 1003 ⚫️ 1004 ⚫️ 1005 ⚫️ 1006 ⚫️ 1007 ⚫️ 1008 ⚫️ 1009
Ví dụ với Concurrent Queue
- Thêm 1 tham số vào hàm tạo Queue
- `attributes` thì mặc định là serial nên chúng ta cần xét thành concurrent
let queue = DispatchQueue(label: "com.fx.myqueue", qos: .utility, attributes: .concurrent)
Kết quả
- Các màu sắc đã in ra lộn xộn và không theo thứ tự cho trước
- Mỗi lần chạy thì sẽ cho ra kết quả khác
🔴 0 🔵 100 ⚫️ 1000 ⚫️ 1001 🔴 1 ⚫️ 1002 🔴 2 ⚫️ 1003 🔴 3 ⚫️ 1004 🔴 4 ⚫️ 1005 🔴 5 ⚫️ 1006 🔴 6 ⚫️ 1007 🔴 7 ⚫️ 1008 🔵 101 🔴 8 ⚫️ 1009 🔵 102 🔴 9 🔵 103 🔵 104 🔵 105 🔵 106 🔵 107 🔵 108 🔵 109
Thêm 1 vấn đề đặt ra nữa là việc tạo ra 1 queue, thêm các task cho nó. Thì auto sẽ thực thi ngay. Và đôi lúc chúng ta muốn gọi queue thực thi lúc nào thì sẽ chạy lúc đó. Vậy giải quyết nó như thế nào?
2. initiallyInactive
Sử dụng đối số là `initiallyInactive` cho tham số attributes
- Nó làm đối số truyền vào hàm khởi tạo 1 queue
- Với đối số này thì queue sẽ không thực thi ngay
- Nó sẽ rất cần thiết cho việc tạo trước các công việc nhưng chưa muốn chúng thực thi
- Việc thực thi lúc nào thì do lập trình viên quyết định
.activate()
thì queue sẽ thực thi các task của nó
Tham khảo code ví dụ sau
let queue = DispatchQueue(label: "com.fx.myqueue", qos: .utility, attributes: .initiallyInactive) queue.async { for i in 0..<10 { print("🔴", i) } } queue.async { for i in 100..<110 { print("🔵", i) } } queue.async { for i in 1000..<1010 { print("⚫️", i) } } print("Do something 1") print("Do something 2") print("Do something 3") queue.activate() print("Do something 4")
Kết quả
- các lệnh in
do something
sẽ chạy trước khiqueue.activate()
- queue được thực thi theo kiểu
serial
Do something 1 Do something 2 Do something 3 🔴 0 Do something 4 🔴 1 🔴 2 🔴 3 🔴 4 🔴 5 🔴 6 🔴 7 🔴 8 🔴 9 🔵 100 🔵 101 🔵 102 🔵 103 🔵 104 🔵 105 🔵 106 🔵 107 🔵 108 🔵 109 ⚫️ 1000 ⚫️ 1001 ⚫️ 1002 ⚫️ 1003 ⚫️ 1004 ⚫️ 1005 ⚫️ 1006 ⚫️ 1007 ⚫️ 1008 ⚫️ 1009
Vậy vừa muốn queue chạy theo ý của mình và vừa muốn chạy theo
concurrent
thì làm sao?
Tham khảo đoạn code sau
- Với tham số
attributes
, thì chúng ta có thể gán cho nó 1array
các giá trị, nên có thể gán vừa cóinitiallyInactive
vàconcurrent
let queue = DispatchQueue(label: "com.fx.myqueue", qos: .utility, attributes: [.initiallyInactive, .concurrent])
3. Delaying the Execution
Cái tên đã nói lên tất cả rồi, ý đồ sẽ là
Delay task
Mục đích:
- Dùng để trì hoãn việc thực thi 1 khối lệnh (công việc) trong 1 block code sau 1 khoản thời gian đặt trước.
Giải pháp:
- sử dụng hàm
asyncAfter(deadline:)
hoặcsyncAfter(deadline:)
- phù hợp cho cả đồng bộ và bất đồng bộ
DispatchTimeInterval:
- Là đơn vị thời gian được sử dụng trong GCD
- Các mức đơn vị có thể sử dụng được
- Seconds
- Microseconds
- Miliseconds
- Nanoseconds
.now()
sẽ là thời điểm hiện lúc thực thi lệnh
Ví dụ thôi
- Tạo 1 queue với công việc là in ra thời gian
- Delay nó 2 giây
- So sánh thời gian in trước đó
let queue = DispatchQueue(label: "com.fx.myqueue", qos: .userInitiated) print(Date()) let additionalTime: DispatchTimeInterval = .seconds(2) queue.asyncAfter(deadline: .now() + additionalTime) { print(Date()) }
Kết quả
- in sau 2 giây so với lệnh in đầu tiên
30 - 28 = 2
2019-11-07 03:43:28 +0000 2019-11-07 03:43:30 +0000
4. Accessing the Main and Global Queues
4.1. Global Queue
Khi bạn mệt mỏi là phải nhớ nhiều tham số khi khởi tạo và sử dụng 1 Queue. Vậy đây là cách đơn giản hơn nhiều để giúp bạn.
Global Queue
Đây cũng là cái mà hầu hết các thanh niên dev iOS sử dụng. Và tầm khoản 80% dùng mà không hiểu hoặc đôi khi hiểu GCD chính là Global Queue
. Ý nghĩa nó như sau:
- Đơn giản cho việc tạo ra queue khi không muốn thay đổi nhiều tham số
- Có thể thêm các tham số về độ ưu tiên giữa các queue với nhau.
- Khi không có thì mặc định chúng là
default
- Mọi thuộc tính khác và delay đều tương tự các queue custom khác
Ví dụ tham khảo
let globalQueue = DispatchQueue.global() globalQueue.async { for i in 0..<10 { print("🔴", i) } }
Thêm thuộc tính cho nó
let globalQueue = DispatchQueue.global(qos: .userInitiated)
4.2. Main Queue
- Hệ thống luôn duy trì
1 main thread
để thực hiện các công việc update UI trong chương trình - Khi tiến hành các công việc liên quan thread, đồng bộ hay bất đồng bộ thì để cập nhật dữ liệu lên giao diện thì chúng ta cần phải truy cập được main thread.
- Thường hay sử dụng sau khi nhận dữ liệu từ API trả về và update lên các TableView, CollectionView…
Cách sử dụng
- Dùng một cách đơn thuần
DispatchQueue.main.async { //code here }
- Dùng kết hợp với Queue khác khi muốn về lại main thread
DispatchQueue.global().async { //do some task DispatchQueue.main.async { //update UI & Data here } }
- Dùng trong cập nhật UI với connect API/Webservice …
-
- tạo đối tượng
url
từ 1 url string - request dữ liệu theo
url
, sử dụngURLSession
–> việc request này sẽ thực thi ở 1 thread khác và diễn ra bất đồng bộ - tạo
UIImage
, từ data nhận được - Update lại
UIImageView
từ main thread
- tạo đối tượng
func fetchImage(urlString: String) { //1 let url = URL(string: urlString) //2 (URLSession(configuration: .default).dataTask(with: url!, completionHandler: { (data, response, error) in if let data = data { //3 let image = UIImage(data: data) //4 DispatchQueue.main.async { self.imageView.image = image } } })).resume() }
5. Using DispatchWorkItem Objects
Toàn bộ các phần trên + bài trước thì nói về việc define các Queue để thực thi công việc. Vậy việc define công việc sẽ như thế nào?
5.1. Định nghĩa
Để define 1 task/1 công việc/1 nhiệm vụ … thì chúng ta sử dụng DispatchWorkItem
- DispatchWorkItem là 1 block code
- Có thể được thêm vào bất kỳ queue nào và thực hiện được trên các thread, background hoặc main thread.
- Giúp đơn giản hoá các mã code để xử lý công việc
- Tách chúng ra các đơn vị nhỏ và dễ quản lý hơn
5.2. Cách sử dụng
let workItem = DispatchWorkItem { // Do something }
5.3. Thực thi
- Muốn cho
workItem
thực thi thì dùng lệnh.perform()
- Khi không ở thread hay queue nào thì
workItem
sẽ được thực thi ởmain thread
var value = 10 let workItem = DispatchWorkItem { value += 5 } print(value) workItem.perform() print(value)
5.4. Thực thi ở trong Queue
let queue = DispatchQueue.global() queue.async { workItem.perform() } print(value)
- Nếu update UI trong
workItem
thì cần phải chú ý. Chương trình sẽ crash nên việc update UI không thực thi ởmain thread
Cách đơn giản hơn là dùng .async(execute:)
let queue = DispatchQueue.global() queue.async(execute: workItem) print(value)
5.5. Notify
Khi 1 workItem được thực thi thì có thể thông báo cho main thread
hoặc bất cứ thread nào:
- tham số
queue
thì quy định Queue nào sẽ nhận được thông báo khiworkItem
thực thi xong
workItem.notify(queue: DispatchQueue.main) { //code here }
Tham khảo code đầy đủ với workItem
var value = 10 let workItem = DispatchWorkItem { value += 5 } workItem.perform() let queue = DispatchQueue.global() queue.async(execute: workItem) workItem.notify(queue: DispatchQueue.main) { print(value) }
6. Handling Background Tasks
Tham khảo 1 đoạn code giả sau:
class ViewController: UIViewController { var bucAnhTuyetDep: UIImageView? override func viewDidLoad() { super.viewDidLoad() //khoi tao anh bucAnhTuyetDep = UIImageView() //xu ly anh let anhTamThoi = hamXuLyAnn() //xet anh bucAnhTuyetDep?.image = anhTamThoi } func hamXuLyAnn() -> UIImage? { return nil } }
Ta thấy:
- Tất cả các công việc đều được xử lý trong
main thread
- Việc xử lý ảnh thường rất tốn thời gian
- Các công việc đặt vào
viewDidLoad
- Giao diện ứng dụng sẽ bị treo cho đến lúc hoàn thành xong việc xử lý ảnh
Và cũng có cách giải quyết kinh điển. Các giải quyết là đưa công việc xử lý ảnh ra một thread khác. Để:
-
- Giảm tải trên main thread
- Tránh block UI
Khi hoàn thành thì cần phải truy cập về Main thread để update lại UI. Tham khảo full code ví dụ:
class ViewController: UIViewController { var bucAnhTuyetDep: UIImageView? override func viewDidLoad() { super.viewDidLoad() //khoi tao anh bucAnhTuyetDep = UIImageView() DispatchQueue.global(qos: .userInitiated).async { [weak self] in guard let `self` = self else { return } //xu ly anh let anhTamThoi = self.hamXuLyAnn() //update UI DispatchQueue.main.async { [weak self] in guard let `self` = self else { return } //xet anh self.bucAnhTuyetDep?.image = anhTamThoi } } } func hamXuLyAnn() -> UIImage? { return nil } }
[weak self]
- Các
weak self
ở đây được sử dụng để tránh các referencestrong
- Khi các đối tượng bị giải phóng thì chương trình cũng không bị crash
- Các
Tóm lại cho việc xử lý task cho background
- Cần thực hiện một cách bất đồng bộ
- Sau khi thực hiện cần truy cập về main thread để thực hiện việc update dữ liệu lên UI nếu cần thiết
- Cần phải quản lý các trạng thái các đối tượng mình sử dụng để tránh việc đối tượng không tồn tại
- Với các tính chất của các DispatchQueue hay độ ưu tiên mà người lập trình có thể quyết định các công việc đó sẽ được thực hiện ở đâu? và thực hiện như thế nào?
Các Queue
- Main Queue : Là sự lựa chọn phổ biến cho việc update UI sau khi hoàn thành task. Mọi thứ với main thì phải là bất động bộ (async)
- Global Queue: Là sự lựa chọn tốt cho các công việc không phải UI và thực thi ở background
- Custom Serial Queue : Là sự lựa chọn tốt nhất cho việc thực hiện ở background và cần theo dõi chúng. Tránh bị chiếm tài nguyên hệ thống. Dùng nhiều trong việc load data.
7. Dispatch Group
Khi chúng ta có rất nhiều task cần thực hiện đồng thời và chúng lại update UI. Vậy phải làm sao để cho chương trình hoạt động một cách mượt mà nhất?
Tham khảo đoạn code sau:
class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() //loadData taskOne() taskTwo() taskThree() } func taskOne() { //code } func taskTwo() { //code } func taskThree() { //code } }
Với việc thực thi các công việc như trên thì:
- Tốn tài nguyên xử lý
- Thực thi cùng lúc
- Không thể kiểm soát được nếu các task đó là bất đồng bộ
Giải quyết
- Để giải quyết yêu cầu bài toán thì GCD đưa ra DispatchGroup
- Nhóm các task cần thực hiện lại, nhất là các task bất đồng bộ
- Quản lý được việc thực thi của các task
- Quyết định được việc chờ đợi để hoàn thành hay không.
Lưu ý: phần DispatchGroup được trình bày ở đây thì phạm vi chỉ là cách sử dụng cơ bản. Và còn rất nhiều các kiến thức nâng cao nữa.
Cài đặt lại các task trong ví dụ trên
- Thêm
call back
, để thông báo việc kết thúc function khi thực hiện bất đồng bộ - Sử dụng thêm các closure để thông báo việc hoàn thành các task
func taskOne(completion: @escaping () -> Void) { print("task one") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { completion() } } func taskTwo(completion: @escaping () -> Void) { print("task two") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { completion() } } func taskThree(completion: @escaping () -> Void) { print("task three") DispatchQueue.main.asyncAfter(deadline: .now() + 3) { completion() } }
Trong ví dụ này chúng ta cài đặt các task với tính chất là bất đồng bộ và thời gian hoàn thành là khác nhau.
Thực thi DispatchGroup
Tiếp tục với ví dụ ở trên và cài đặt DispatchGroup vào function viewDidLoad
DispatchQueue.global(qos: .userInitiated).async { //1 let dispatchGroup = DispatchGroup() //2 dispatchGroup.enter() //3 self.taskOne { print("ONE -> DONE") //4 dispatchGroup.leave() } dispatchGroup.enter() self.taskTwo { print("TWO -> DONE") dispatchGroup.leave() } dispatchGroup.enter() self.taskThree { print("THREE -> DONE") dispatchGroup.leave() } //5 dispatchGroup.notify(queue: .main) { print("ALL DONE") } }
Trong đó:
- Tạo ra một đối tượng DispatchGroup và cần có 1 queue để thực thi các task của DispatchGroup
- Đưa 1 task vào group để thực thi, sử dụng
.enter()
- Thực thi task đó
- Thoát task ra khỏi DispatchGroup, sử dụng
.leave()
- Sau khi hoàn thành tất cả các task thì Group sẽ báo 1 notify về main queue (hoặc có thể bất kỳ queue nào). Cần chú ý về notify tới thread nào.
Chú ý 1 điều rất quan trọng là: “bao nhiêu lần
.enter()
, thì bấy nhiều lần.leave()
“.Nếu như số lần
leave
ít hơnenter
thì DispatchGroup sẽ không bao giờ kết thúc. Để giải quyết đềtimeout
này thì chúng ta sử dụng hàmwait()
.
wait()
- Hàm
wait()
sẽ dừng toàn bộ thread của DispatchGroup lại cho đến khi các task được thực thi xong hết - Ngoài ra, còn có các hàm
wait(timeout: )
để người lập trình xét thời gian cho việc hoàn thành các task. - Nếu các task chưa kịp hoàn thành thì các lệnh trong hàm
wait(timeout: )
sẽ được thực thi
DispatchQueue.global(qos: .userInitiated).async { let dispatchGroup = DispatchGroup() dispatchGroup.enter() self.taskOne { print("ONE -> DONE") dispatchGroup.leave() } dispatchGroup.enter() self.taskTwo { print("TWO -> DONE") dispatchGroup.leave() } dispatchGroup.enter() self.taskThree { print("THREE -> DONE") dispatchGroup.leave() } dispatchGroup.wait() dispatchGroup.notify(queue: .main) { print("ALL DONE") } }
Okay, tới đây thì hàng họ cũng đã đủ để chiến đấu vô tư với GCD rồi. Chúc bạn may mắn!
Tạm kết
- Bất đồng bộ với Concurrent Queues
- Delaying thực thi task trong Queue
- Sử dụng Global Queue và Main Queue
- Định nghĩa công việc với WorkItem
- Xử lý Background Tasks
- Thao tác thực thi và quản lý nhiều task với Dispatch Group
“Kết thúc phần lý thuyết này chỉ là cơ bản về GCD và cách sử dung nó. Các phần nâng cao hơn sẽ được cập nhật vào trong thời gian không xa.”
Related Posts:
Written by chuotfx
Hãy ngồi xuống, uống miếng bánh và ăn miếng trà. Chúng ta cùng nhau đàm đạo về đời, về code nhóe!
Leave a Reply Cancel reply
Fan page
Tags
Recent Posts
- Charles Proxy – Phần 1 : Giới thiệu, cài đặt và cấu hình
- Complete Concurrency với Swift 6
- 300 Bài code thiếu nhi bằng Python – Ebook
- Builder Pattern trong 10 phút
- Observer Pattern trong 10 phút
- Memento Pattern trong 10 phút
- Strategy Pattern trong 10 phút
- Automatic Reference Counting (ARC) trong 10 phút
- Autoresizing Masks trong 10 phút
- Regular Expression (Regex) trong Swift
You may also like:
Archives
- September 2024 (1)
- July 2024 (1)
- June 2024 (1)
- May 2024 (4)
- April 2024 (2)
- March 2024 (5)
- January 2024 (4)
- February 2023 (1)
- January 2023 (2)
- November 2022 (2)
- October 2022 (1)
- September 2022 (5)
- August 2022 (6)
- July 2022 (7)
- June 2022 (8)
- May 2022 (5)
- April 2022 (1)
- March 2022 (3)
- February 2022 (5)
- January 2022 (4)
- December 2021 (6)
- November 2021 (8)
- October 2021 (8)
- September 2021 (8)
- August 2021 (8)
- July 2021 (9)
- June 2021 (8)
- May 2021 (7)
- April 2021 (11)
- March 2021 (12)
- February 2021 (3)
- January 2021 (3)
- December 2020 (3)
- November 2020 (9)
- October 2020 (7)
- September 2020 (17)
- August 2020 (1)
- July 2020 (3)
- June 2020 (1)
- May 2020 (2)
- April 2020 (3)
- March 2020 (20)
- February 2020 (5)
- January 2020 (2)
- December 2019 (12)
- November 2019 (12)
- October 2019 (19)
- September 2019 (17)
- August 2019 (10)