Concurrent programming with GCD in Swift 3

109
Concurrent programming là một khái niệm không gì xa lạ trong thế giới lập trình ứng dụng. GCD – Grand Central Dispatch là một trong những cách để lập trình multithreading (tạm dịch là đa luồng). Đặc biệt trong Swift 3 với sự thay đổi lớn về cú pháp cũng như Xcode 8 đã thêm vào những công cụ để debug thread hiệu quả hơn. Thông qua bài viết này, IDEA hy vọng sẽ giúp các bạn nắm vững được các kỹ thuật cơ bản về concurrent programming để có thể nhanh chóng đưa vào ứng dụng trong các ứng dụng sẽ cần update lên Swift 3 sắp tới 🙂

Threads

Thread là đơn vị nhỏ nhất trong concurrent programming. Một trong những đặc tính cơ bản nhất là chúng giúp chúng ta có thể thực thi nhiều công việc (task) trong cùng một lúc.

Minh họa về Threads

Grand Central Dispatch

GCD là một tập hợp, một thư viện để cải tiến và đơn giản hóa việc dùng Thread. Chúng ta sẽ không còn phải tạo tường minh các thread rồi start chúng, thay vào đó chúng ta sẽ đưa những công việc (task) vào các queue. Các queue này sẽ quản lý thread (khởi tạo, chạy và hủy thread).
  • GCD hoạt động dựa trên cơ chế Thread Pool: đây là cơ chế giúp tối ưu việc tạo và hủy threads. Hãy tưởng tượng trong một quán ăn có 10 bàn thì không nhất thiết phải có 10 người phục vụ. Thay vào đó ta chỉ cần đâu đó 3 – 4 người là đủ.

Dispatch Queues

Các dispatch queues có nhiệm vụ tạo và hủy threads, thực thi các task được đưa vào. Một số đặc điểm của Dispatch Queues:
  • Có 2 loại: SerialConcurrent.
    1. Serial là queue chỉ có duy nhất 1 thread nên chúng chỉ có thể chạy được 1 tác vụ trong một thời điểm. Main queue là dispatch queue đặc biệt, có sẵn trong hệ thống và cũng là serial queue.
    2. Concurrent là queue có nhiều hơn 1 thread nên có thể chạy một lúc nhiều tác vụ song song. Global queue là concurrent queue và có sẵn trong hệ thống.
  • Các dispatch queues sẽ thực thi task theo thứ tự FIFO (First In First Out).

Dispatch Async

Một trong những cách để dùng GCD đó là sử dụng Dispatch Async. Cách này dùng để thực thi các tác vụ bất đồng bộ với nhau (không ai đợi ai, việc ai nấy làm)
// Vì là serial queue nên ta sẽ thấy chúng lần lượt in ra màn hình từ 1 đến 5, mỗi lần cách nhau 1s. Nếu các bạn dùng với concurrent queue, 5 task này sẽ thực thi cùng 1 lúc.

Dispatch Sync

Khác với Dispatch Async, Dispatch Sync thực thi các tác vụ một cách đồng bộ với nhau. Tại sao lại cần sự đồng bộ này nhỉ ?!? Đó là trong thế giới concurrent programming, các tác vụ có những trường hợp chia sẻ tài nguyên với nhau. Thông thường chúng xảy ra khi các tác vụ (các thread) cùng edit một biến nào đó.
Vòng lặp trên đại ý sẽ chạy 1000 lần, mỗi lần sẽ tăng count lên 1. Chúng ta có 1000 tác vụ chạy gần như song song (vì ta có thread pool nên chắc chắn sẽ không thể có 1000 threads được tạo ra, con số thực tế là rất ít, tùy vào hệ thống). Kết thúc vòng for này, màn hình chỉ in ra đâu đó 990 – 997, chắc chắn không thể là 1000 !!!
Lý do khá đơn giản là trong 1 thời điểm nhất định nào có, sẽ có vài threads cùng đọc và ghi gía trị cho biến count. Vì là cùng 1 lúc nên giả sử count đang là 10, các thread sẽ cùng ghi vào giá trị 11 cho biến count. Từ dó sẽ có những thời điểm biến count cập nhật lại giá trị cũ. Vấn đề này còn được biết đến với thuật ngữ Data Race.
Để fix được điều này ta chỉ cần sửa 1 dòng trong đoạn code trên:
concurrentQueue.sync(execute: { // thay async thành sync, chạy lại đoạn code trên ta thấy màn hình đã in ra đc 1000

Thread Sanitizer

Đây là một công cụ mới trong XCode 8, nó giúp ta detect được những chỗ bị data race trong code. Đây thật sự là công cụ rất hữu ích vì bug này thật sự rất khó có thể bắt được. Cách để enable Thread Sanitizer:

Chọn edit schema, check vào option Thread Sanitizer trong phần Diagnostic

Khi run ứng dụng, data race sẽ được detect nếu có

Deadlock

Dispatch Sync về mặt cơ chế muốn đồng bộ dữ liệu sẽ phải lock thread hiện tại và các threads trong cùng queue với nó để thực thi tác vụ cần đồng bộ, sau khi tác vụ này được thực thi, nó sẽ unlock các threads.
Trong serial queue, nếu ta sử dụng dispatch sync phải cực kỳ cẩn thận. Bởi vì queue này chỉ có 1 thread duy nhất, nếu ta lock nó để thực thi tác vụ dispatch sync thì … ai sẽ là người thự thi tác vụ đó vì nó bị lock mất rồi. Đó chính là deadlock, thread bị lock mãi mãi không thể unlock được nữa.
Ngoài ra chúng ta còn một trường hợp deadlock nữa đó là nếu thread A phụ thuộc vào thread B, nhưng thread B lại phụ thuộc vào Thread A. Phụ thuộc ở đây nghĩa là 2 thread này đợi nhau thực thi xong tác vụ của mình. Điều này cũng giống nhau khi ra lỡ tay quăng chìa khóa vào cốp xe máy rồi đóng cốp xe lại: ta cần mở cốp để lấy chìa khóa nhưng lại cần dùng chìa khóa để mở cốp !!!

** Trong Xcode 8, dealock sẽ gây lỗi và crash app chứ không đợi mãi như XCode 7.

Dispatch After

Dispatch after dùng để hẹn giờ cho 1 tác vụ nào đó được thực thi sau 1 quãng thời gian nhất định. Trong Swift 3, dispatch after đã dễ dàng khai báo hơn như sau:

DispatchWorkItem

Kể từ Swift 3, chúng ta có thể không nhất thiết phải dùng closure để tạo các tác vụ, thay vào đó Apple đã bổ sung 1 class mới mang tên DispatchWorkItem:

====================

Tóm lại trong Swift 3, Apple đã thay đổi lại hoàn toàn cú pháp khi báo của GCD. Thật ra điều này chúng ta đã được biết đến trước đó, là những hàm C hoặc cấu trúc của C sẽ được wrap vào các đối tượng (hoặc struct/enum). Điều này khiến source code Swift 3 sẽ dễ khai báo và dễ đọc hơn.
Trong phần tiếp theo, IDEA sẽ giới thiệu các bạn dispatch group, quality of service và barrier trong GCD :).
Video về GCD trong Swift 3 của IDE Academy: https://www.youtube.com/watch?v=nuk…