iOS Concurrency - Phần 2: Những thuật ngữ và vấn đề hay gặp phải trong Concurrency.

Thuật ngữ (Terminologies)

Để có thể hiện thực concurrency, chúng ta cần hiểu một số khái niệm liên quan đến chúng. Dưới đây là một số những thuật ngữ hay gặp phải khi chúng ta lập trình concurrency.

Serial, Concurrency and Parallelism

Ba thuật ngữ trên mô tả cách mà những tasks chạy (execute). Khi chúng ta nói những tasks này chạy tuần tự (execute serially) có nghĩa là chỉ có 1 task được chạy tại một thời điểm. Concurrency và Parallelism đều có nghĩa là nhiều tasks chạy đồng thời tại cùng một thời điểm. Như vậy Concurrency và Parallelism khác nhau những gì? Ứng dụng iOS bao gồm một hoặc nhiều thread và những threads này được quản lý bởi scheduler của hệ điều hành(OS). Scheduler này sẽ quyết định khi nào và cách mà mỗi thread chạy. Những thiết bị nhiều lõi (multi-core) chạy nhiều threads tại một thời điểm thông qua cơ chế parallelism. Tuy nhiên để thiết bị một lõi (single core) có thể đạt được đặt tính concurrency này, chúng ta phải chạy 1 thread sau đó thực hiện chuyển ngữ cảnh (context switch) và chạy một thread khác. Context switch sẽ lưu trữ trạng thái (execution state) giữa những threads khác nhau để phục vụ cho việc chạy lại khi hệ điều hành yên cầu. Hệ điều hành sẽ chạy mỗi thread cho một lượng nhỏ của milisecond và chuyển đỗi giữa chúng. Điều này cho chúng ta một ảo tưởng rằng chúng ta đang chạy song song (parallel). Người ta hay gọi cơ chế này là time-slicing hay fake-multitasking. Hình dưới đây sẽ cho chúng ta thấy một hình dung rõ ràng hơn.

Synchronous và Asynchronous (Đồng bộ và bất đồng bộ)

Hai thuật ngữ trên được dùng để mô tả cách một hàm (function) hoạt động. Một hàm đồng bộ (synchronous function) sẽ trả về khi công việc được hoàn thành. Ngược lại, một hàm bất đồng bộ (asynchronous function) trả về ngay lập tức, mặc dù nó yêu cầu công việc phải được làm hết nhưng nó không đợi cho tới khi tất cả công việc xong mới trả về. Do đó một hàm asyn (asynchronous function) sẽ không block thread hiện tại của chương trình và để cho thread tiếp theo tiếp tục chạy.

Concurrency Problems:

Race condition và critical section:

Race condition là trạng thái mà hai hay nhiều luồng (thread) cùng truy xuất, thay đổi tài nguyên dùng chung (shared resource) một cách đồng thời. Tài nguyên dùng chung (shared resource) ở đây có thể là một thuộc tính (property), một object, một file, memory, … Bất cứ mọi tài nguyên dùng chung nào mà được chia sẽ giữa nhiều thread đều tìm ẩn nguy cơ xung đột (conflict). Để chứng minh cho vấn đề này, chúng ta cùng xem 1 ví dụ nho nhỏ mà tài nguyên dùng chung là một số Integer được sử dụng như một biến đếm. Giả sử chúng ta có 2 thread A, B chạy đồng thời và cả hai cùng tăng biến đếm một lúc. Để tăng biến đếm bạn phải đọc (read) giá trị từ memory, sau đó cộng biến đếm thêm một (increment) và viết (write) giá trị xuống memory. Như bạn thấy ở hình ảnh bên dưới, thread A đọc giá trị biến đếm từ memory lên là 17 để tiến hành tăng biến đếm thành 18. Thread A chưa kịp viết (write) giá trị biến đếm mới vào memory thì thread B đọc giá trị biến đếm cũ là 17 tăng lên 1. Sau đó cả hai cùng viết giá trị 18 xuống memory. Kết quả chúng ta mong muốn sau 2 lần tăng biến đếm là 19 nhưng kết quả ghi xuống lại là 18. Vấn đề này được gọi là race condition và nó luôn luôn diễn ra nếu nhiều thread truy cập tài nguyên dùng chung mà không đảm bảo rằng một thread đã kết thúc các hoạt động trên tài nguyên dùng chung trước khi một thread khác bắt đầu truy xuất nó. Critical Section chính là phần code mà không thể được chạy đồng thời từ hai hay nhiều luồng cùng một lúc để đảm bảo không xảy ra Race condition.

Mutual Exclusion

Mutual Exclusive Access là một cách truy cập mà chỉ có một thread tại một thời điểm truy cập tới một tài nguyên nhất định. Để đảm bảo điều này, mỗi thread mà muốn truy cập một tài nguyên phải có một cái khoá (mutex lock). Một khi thread này kết thúc hoạt động, nó trả lại cái khoá và một thread khác lấy nó để truy cập. Bạn có thể xem hình ảnh dưới này để hiểu cơ chế hoạt động. Đây cũng chính là cách mà chúng ta giải quyết race condition.

Dead lock

Mutex locks giải quyết vấn đề race condition nhưng không may mắn thay nó lại nảy sinh ra một vấn đề khác là deadlock. Một deadlock diễn ra khi nhiều thread đang đợi lẫn nhau để hoàn thành (finish) công việc dẫn tới tắc nghẽn (stuck). Hình vẽ trên là một ví dụ điển hình về deadlock. Thread 1 không thể finish được bởi vì nó đang đợi thread 2 finish. Nhưng thread 2 cũng không thể finish được vì nó đang đợi thread 1 finish.

Thread Safe

Những đoạn code an toàn về luồng là những đoạn code được hiện thực nhằm đảm bảo không có race condition xảy ra khi được truy cập bởi nhiều thread một cách đồng thời.

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 đã nắm được một số thuật ngữ cũng như vấn đề hay gặp phải khi chúng ta lập trình concurrency. Ở bài tiếp theo mình sẽ giới thiệu một kỹ thuật để hiện thực concurrency trong iOS đó là Grand Central Dispatch (GCD). Hẹn gặp các bạn ở những bài viết kế tiếp trong chuỗi series về iOS concurrency.

Tài liệu tham khảo

https://www.raywenderlich.com/60749/grand-central-dispatch-in-depth-part-1 https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1 https://www.objc.io/issues/2-concurrency/concurrency-apis-and-pitfalls/