Concurrent Programming và GCD (Part 2)

brian
7 min readMar 21, 2018

--

Chào mọi người !! Đây là phần tiếp theo của bài viết “Concurrent Programming và GCD”. Để xem lại phần trước, mọi người có thể bấm vào đây Concurrent Programming và GCD (Part 1). Trong nội dung bài viết này, mình sẽ giới thiệu qua các công nghệ được sử dụng để lập trình multi-threading trong iOS, và GCD là nội dung mình sẽ tập trung vào để giải thích, một số công nghệ khác sẽ nằm ở mục đích giới thiệu.

Cũng giống như mọi ngôn ngữ lập trình cho các flatform khác. Thì với iOS, ta cũng có thể tự tạo cho riêng mình các thread để quản lý việc lập trình multi threads. iOS cung cấp sẵn cho chúng ta các công cụ như : NSThread, POSIX.

NSThread

NSThread là một thư viện hướng đối tượng được cung cấp sẵn trong Foundation framework. Chúng ta có thể sử dụng NSThread để tự tạo ra số lượng các threads tuỳ ý.

POSIX

POSIX, hay còn được biết với cái tên là pthread, là một thư viện C-based, nhờ vậy, POSIX có thể hỗ trợ cho rất nhiều ứng dụng trên các nền tảng khác nhau. Nó khá tiện ích trong việc, nếu bạn muốn làm ứng dụng multiflatform.

Điểm hạn chế của các thư viện trên là chúng ta phải tạo ra một số lượng threads và quản lý chúng một cách thủ công. Điều này gây ra vô số sự bất tiện và ảnh hưởng trực tiếp đến performance của app, sẽ không thật sự tối ưu nếu chúng ta tạo ra quá nhiều threads mà không sử dụng hết.

Grand Central Dispatch aka GCD

Hãy tạm thời quên những thứ ở trên đi, vì chúng ta đã có GCD :)) GCD giải quyết gần như triệt để khuyết điểm của các thư viện nói trên. Và đây cũng là phần trọng tâm mình muốn nói đến trong bài viết này.

Really nigga ?????

Vậy trước hết GCD là gì ??? GCD là một low-level API dùng để quản lý các tác vụ chạy đồng thời, được giới thiệu lần đầu tiên vào năm 2009 trên iOS 4.0 OS X 10.6 (cái này mới search cho thông tin thêm phong phú :3). Có thể hiểu GCD là một công cụ sinh ra để hỗ trợ cho việc lập trình concurrent được dễ dàng hơn. Vậy thì GCD có gì hấp dẫn??? Vâng! GCD được thiết kế dựa trên Thread Pool Pattern (các bạn có thể xem thêm về định nghĩa của Thread Pool tại đâybài viết). Nhờ vào GCD, chúng ta có thể quên đi việc quản lý các threads mà chúng ta tạo ra, bởi vì GCD sẽ làm điều đó. Chúng ta sẽ không tương tác trực tiếp với các threads, GCD sẽ quản lý việc có bao nhiêu threads được tạo ra, điều này phụ thuộc vào tài nguyên của hệ thống. Thậm chí GCD sẽ điều chỉnh lại số threads khi hệ thống tắt bớt các core để tiết kiệm pin. Sử dụng GCD, chúng ta có thể tận dụng được các đặc trưng của thread nhưng không cần quan tâm đến thread management. Vậy nếu không làm việc với thread thì chúng ta sẽ lập trình concurrent như thế nào???

Huh????

GCD sử dụng cấu trúc dữ liệu queue (cái này chắc ai cũng biết rồi :)) FIFO) và đơn vị nhỏ nhất để sử dụng trên queues đó gọi là các tasks (có thể hiểu là blocks of code). Thay vì tạo ra một đống các threads như trước, thì giờ đây chúng ta sẽ tạo ra một đống các queues, và trong các queue đó sẽ chứa các tasks. Vậy là đã rõ, chúng ta sẽ làm việc trực tiếp với các tasks, còn việc các tasks đó được chạy trên thread nào thì sẽ do GCD quyết định. Uhm!!! Theo cái Thread Pool đã nói ở trên. GCD cung cấp cho chúng ta các dispatch queue để thiết lập cơ chế trên, và các dispatch queue này đều là thread-safe, do vậy chúng có thể được access từ nhiều thread khác nhau mà không lo xảy ra lỗi. Có 2 loại dispatch queue đó là: serial queueconcurrent queue.

  • Serial Queue: mỗi lần thực hiện một task duy nhất, các task sẽ được thực hiện theo đúng thứ tự lúc thêm vào queue. Task chỉ dược thực thi khi task trước đó đã hoàn thành.
  • Concurrent Queue: các tasks được đảm bảo sẽ thực thi theo đúng thứ tự nhưng không đảm bảo các task sẽ hoàn thành theo đúng thứ tự ban đầu.

Synchronous và Asynchronous

Bên cạnh hai loại queue kể trên, chúng ta cũng có hai loại task phổ biến trong GCD là synchronous và asynchronous.

Synchronous: sẽ return chỉ sau khi task được hoàn thành. Có nghĩa là khả năng block thread hiện tại nếu sử dụng để thực thi một task tốn nhiều thời gian (request APIs, download media content,…)

Asynchronous: sẽ return ngay lập tức mà không cần phải chờ. Dễ hiểu hơn là nó sẽ add task vào queue vào tiếp tục thực hiện các công việc khác mà không gây block. Ví dụ:

/// Pseudo code 
print("Test 1")
synchronous {
wait(10)
print("Test 2")
}
print("Test 3")

Với đoạn code trên thì output sẽ là:

Test 1
Test 2
Test 3

Trường hợp asynchronous

/// Pseudo code 
print("Test 1")
asynchronous {
wait(10)
print("Test 2")
}
print("Test 3")

Output sẽ là:

Test 1
Test 3
Test 2

Nói tóm lại nếu đang chạy một đoạn code nào đó nó sẽ bị đứng lại ở sync và chỉ tiếp tục chạy khi sync return , còn với async thì ngược lại, đoạn code đó sẽ tiếp tục chạy.

Hiện tại, trong system đã cung cấp cho chung ta sẵn 5 dispatch queues: 1 serial queue hay còn gọi là main queue và 4 concurrent queues hay còn gọi là các global queues. Mặc định, chúng ta có thể sử dụng các queues đó hoặc tạo queue mới. Việc bất tiện của các queues có sẵn này là chúng được sử dụng chung, nếu chúng ta cần 1 queue riêng biệt để xử lý một nhiệm vụ cụ thể nào đó thì nên tự tạo ra một custom queue rồi làm việc trên đó.

Lưu ý là main queue chỉ nên được sử dụng để update UI. Vì các tasks trên main queue sẽ được thực thi trên main thread và nhiệm vụ chính của main thread là update UI và quản lý view cycle (view Loop).

Các 4 global queues tương ứng với 4 độ ưu tiên khác nhau: high, low, default, background. Background có độ ưu tiên thấp nhất. Chúng ta cũng có thể tạo ra các custom queue với 4 độ ưu tiên như trên, mặc định custom queue được tạo ra sẽ có độ ưu tiên là default.

Một số API thường dùng của GCD

Các API này sẽ viết trên syntax của Objective-C. Swift thì tên vẫn tương tự nhưng syntax đổi một tí.

  • dispatch_async : submit một block vào queue và return lại ngây lập tức, main thread vẫn tiếp tục chạy không gây hiện tượng block UI hay freezing UI
  • dispatch_after : delay task một khoảng thời gian trước khi thêm vào queue. Hoạt động như một delayed dispatch_async
  • dispatch_once : thực thi block 1 lần duy nhất trong cả quá trình chạy của ứng dụng.
  • dispatch barrier api : đảm bảo rằng task khi submit vào queue thì chỉ có chính nó là đang thực thi trong queue. Các tasks được submit trước đó đã được hoàn thành hết trước khi task của barrier được thực thi, và trong lúc task này thực thi thì các task khác sẽ phải chờ cho đến khi task của barrier hoàn thành và queue sẽ trở lại trạng thái ban đầu là sử lý concurrency.
  • dispatch_sync : submit task và block sẽ return khi task đc hoàn thành. Block main thread.
  • dispatch_queue_create : tạo ra một custom queue với 2 tham số đầu vào, arg1 là tên của queue thường được viết theo DNS style naming convention, arg2 là loại queue : 0 hoặc NULL nếu tạo serial queue và DISPATCH_QUEUE_CONCURRENT nếu tạo concurrent queue.
  • dispatch group API : sẽ thông báo khi tất cả các task của group được hoàn thành. Các tasks có thể là async hoặc sync và có thể track từ các queues khác nhau.
  • dispatch_apply : hoạt động như một loop, nhưng các lần lặp được thực thi đồng thời. dispatch_apply là sync func. Có 3 tham số , 1st arg số iterations, 2nd arg queue thực thi các task, 3rd là block code. Độ hiệu quả của dispatch_apply phụ thuộc vào độ phức tạp trong mỗi iteration.
  • dispatch_set_target_queue: điều chỉnh độ ưu tiên của custom queue.
  • dispatch_suspend/dispatch_resume : suspend/resume thực thi của một queue.

Cũng như lần trước thì mình xin cảm ơn các bạn đã dành thời gian để đọc hết những dòng mình viết ra. Hẹn mọi người vào bài viết sau. Nếu có bất cứ sai sót gì trong bài thì mọi người hãy comment để cho mình biết và chỉnh sửa nhé ❤

Mình khuyến cáo các bạn đọc thêm hai bài viết của Ray Wenderlich: Part 1 & Part 2. Các bài viết được viết rõ ràng và có ví dụ cho Swift.

--

--

brian

a software engineer who does software engineering