Race Condition và giải pháp trong 10 phút – Swift
iOS & Swift . TutorialsContents
Chào mừng bạn đến với Fx Studio. Bài viết này thuộc về vũ trụ Concurrency trong Swift. Chủ đề là vấn đề mà bạn sẽ gặp thường xuyên trong quá trình lập trình. Nhất là khi bạn thao tác với đa luồng (multi-threading). Đó là Race Condition. Và chúng ta sẽ tìm hiểu cách giải quyết chúng.
Series bài về vũ trụ Concurrency của Fx Studio thì đã có 2 bài viết trước rồi. Bạn có thể tham khảo nó ở link dưới đây.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Vì là bài viết mang tính chất kiến thức căn bản, nên sẽ không cần có yêu cầu gì về mặt version cho các OS và tools. Bạn chỉ cần đã thông được các kiến thức liên quan sau:
Bài viết này có thể xem là bài viết nâng cao tiếp theo của Grand Central Dispatch (GCD). Và nó cũng là bài viết chuẩn bị cho bạn tiếp tục dấn thân vào Swift New Concurrency Roadmap.
Về mặt demo, chúng ta sẽ làm việc với console là chính. Bạn cần tạo mới một iOS Project để tận dụng thêm khả năng Concurrency và có sẵn Main Thread giúp bạn. Vì với PlayGround, nhiều bạn sẽ khó hình dung ra lắm. (tin mình đi)
Race Condition là gì?
Định nghĩa
Theo Wikipedia, ta có khái niệm của nó như sau:
A race condition or race hazard is the condition of an electronics, software, or other system where the system’s substantive behavior is dependent on the sequence or timing of other uncontrollable events. It becomes a bug when one or more of the possible behaviors is undesirable.
Với ngôn ngữ lập trình nói chung hay với Swift nói riêng, thì như sau:
A race condition occurs when two or more threads can access shared data and they try to change it at the same time.
Race condition xảy ra khi hai hoặc nhiều thread truy cập tới cùng 1 shared data và cố gắng thay đổi nó cùng một lúc. Hay lại có khi hai hay nhiều Thread cùng chia sẻ dữ liệu, hay đơn giản là cùng đọc và ghi vào một vùng dữ liệu.
Khi đó vấn đề xảy ra là: Kết quả của việc thực thi multiple threads có thể thay đổi phụ thuộc vào thứ tự thực thi các thread.
Nguyên nhân
Ta hãy xem ví dụ kinh điển nhất khi bạn xử lý đa luồng cho vấn để truy cập và cập nhật dữ liệu.
Với hình trên, bạn sẽ thấy
- Công việc của ta là sẽ thay đổi giá trị của 1 biến Integer lần lượt tại 2 Thread
- Thread 1 thực hiện xong, thì đến Thread 2
- Mọi việc xảy ra êm đềm nếu 2 chúng nó không cạnh tranh nhau.
Vấn đề chỉ xảy ra khi có sự cạnh tranh giữa các Thread.
Khi các luồng chạy đồng thời và bất đồng bộ. Thì có sự cập nhật dữ liệu không nhất quán với nhau. Như ví dụ
- Cả 2 đều đọc giá trị ban đầu là
0
. - Sau đó cả 2 đều tăng giá trị của biến. Kết quả tại mỗi Thread sẽ phụ thuộc vào giá trị đọc được ở bước trước.
- Kết quả trả về thì chúng ta nhận được là
1
, nó không giống như mong đợi là2
Tóm tắt lại, Race Condition sẽ gây ra tác dụng không mong muốn chính là về giá trị dữ liệu không được như kì vọng ban đầu. Đôi khi với các kiểu dữ liệu tham chiếu sẽ gây ra chết chương trình.
Race condition nói về: Vấn đề sai sót về mặt thời gian hoặc thứ tự thực thi của các thread trong chương trình khiến cho kết quả cuối cùng không đúng như mong muốn.
Ví dụ
Ví dụ #1
Đi qua phần lý thuyết và giải thích mệt mỏi rồi. Chúng ta sẽ lấy một ví dụ code cho dễ thông não. bạn hãy xem đoạn code sau:
class ViewController: UIViewController { let concurrentQueue = DispatchQueue(label: "com.fx.queue1", attributes: .concurrent) var number = 0 override func viewDidLoad() { super.viewDidLoad() // Thread 1 concurrentQueue.async { for _ in 0...10 { self.number += 1 print("🔴: \(self.number)") } } // Thread 2 concurrentQueue.async { for _ in 0...10 { self.number += 1 print("🔵: \(self.number)") } } // show result after 3.0 secs DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { print("Number = \(self.number)") } } }
Trong đó:
- Concurrent Queue sẽ giúp tác vụ của bạn chạy trên các Thread khác nhau.
- Ta sẽ có 2 Thread để cùng nhau thay đổi giá trị của biến
number
Kết quả ra như sau:
Bạn để ý xem các ô màu đỏ.
- Thread 1 đọc
number = 2
, sau đó nó tăng lên là8
. - Thread 2 chạy sau với giá trị
number = 7
, nhưng sau đó lại thành18
Mỗi lần chạy thì sẽ cho ra các tiến trình khác nhau. Tuy nhiên, chúng vẫn không chết chương trình. Vì Integer là kiểu dữ liệu không an toàn. Và ta đang dùng nó như một share data
.
Ví dụ #2
Ta sẽ chuyển sang kiểu dữ liệu là Dictionary để tăng độ khó cho game nha. Bạn thêm một thuộc tính sau vào class.
var strings: [String:Int] = [:]
Mình sẽ dùng nó để lưu trữ lại giá trị từng bước của biến number
và của Thread nào. Bạn xem thêm phần code để lưu trữ như sau:
// Thread 1 concurrentQueue.async { for _ in 0...10 { self.number += 1 self.strings["🔴 \(self.number)"] = self.number print("🔴: \(self.number)") } } // Thread 2 concurrentQueue.async { for _ in 0...10 { self.number += 1 self.strings["🔵 \(self.number)"] = self.number print("🔵: \(self.number)") } } // show result after 3.0 secs DispatchQueue.main.asyncAfter(deadline: .now() + 3.0) { print("Number = \(self.number)") for item in self.strings { print(item) } }
Bạn hãy thực thi chương trình và sẽ nhận được kết quả như sau
EXC_BAD_ACCESS
Mình đã cố tình tạo ra key
khác nhau cho mỗi item của Dictionary rồi. Tuy nhiên, vẫn bị crash tại chỗ xét dữ liệu cho nó. Do đó, bạn cũng đã thấy được ngoài việc đưa ra kết quả không mong muốn. Thì nghiêm trọng còn dẫn tới crash chương trình nữa.
Tuy nhiên, các lỗi này sẽ không giống nhau ở tất cả lần chạy. Hoặc đôi lúc chương trình sẽ không bị crash. Vì nguyên nhân là GCD sẽ phó mặc cho hệ thống quyết định. Đôi lúc hên là mọi thứ sẽ chạy lần lượt với nhau.
Khi bạn không hiểu về bản chất của Multi-Threading, thì sẽ gây ra nhiều hậu quả không đáng có.
Giải pháp
Với ngôn ngữ Swift, ta sử dụng GCD để đưa ra một số giải pháp sau. Mục đích để khắc phụ các lỗi không đáng có do Race Condition gây ra.
Serial Queue
Khi mà Concurrent Queue gây ra lỗi như vậy, thì tại sao chúng ta phải dùng nó?. Trong khi, người anh em của nó là Serial Queue không tốt hơn sao.
Vậy, giải pháp đầu tiên chính là thay thế bằng Serial Queue. Và chúng ta xem lại ví dụ đã được thay đổi sang Serial Queue sẽ như thế nào.
Bắt đầu, bạn khai báo thêm 1 Serial Queue như sau:
- Mặc định với việc tạo mới một DispatchQueue thì nó sẽ là
serial
let serialQueue = DispatchQueue(label: "com.fx.queue2")
Tiếp theo, ta sẽ thay biến concurrentQueue
bằng serialQueue
cho ví dụ ở trên.
// Thread 1 serialQueue.async { for _ in 0...10 { self.number += 1 self.strings["🔴 \(self.number)"] = self.number print("🔴: \(self.number)") } } // Thread 2 serialQueue.async { for _ in 0...10 { self.number += 1 self.strings["🔵 \(self.number)"] = self.number print("🔵: \(self.number)") } }
Thực thi chương trình và mọi thứ rất là mượt. Nguyên nhân vì:
- Việc thực hiện các tác vụ trong Serial Queue thì sẽ diễn ra lần lượt với nhau.
- Tác vụ này xong thì sẽ tới tác vụ khác
- Không có sự cạnh tranh gì ở đây hết
Và bạn sẽ thấy serialQueue.async
thì khá là dư thừa rồi. Vì bản thân nó đã xử lý theo serial rồi. Ahihi!
Serial queue có thể giúp bạn ngăn chặn Race condition một cách thiểu năng nhất (nó chạy 1 luồng rồi, cứ task nào xong chạy tiếp task kia thôi thì ngại gì vết bẩn) nhưng đổi lại là performance của nó đương nhiên sẽ không tốt bằng việc chạy nhiều luồng (Concurrent queue) nếu bạn biết cách sử dụng Concurrent queue 1 cách hợp lý.
Synchronous
Việc xử lý đồng bộ (sync
) giúp chúng ta chắc chắn rằng task được chỉ định phải thực hiện xong, đồng thời nghĩa là task kế tiếp phải đợi cho tới khi task hiện tại đã hoàn thành xong hết mới được chạy.
Đó là giải pháp thứ 2, cho Concurrent Queue (ban đầu) chạy đồng bộ. Và bạn xem lại ví dụ mới như sau:
- Thay
async
(ban đầu) bằngsync
// Thread 1 concurrentQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔴 \(self.number)"] = self.number print("🔴: \(self.number)") } } // Thread 2 concurrentQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔵 \(self.number)"] = self.number print("🔵: \(self.number)") } }
Build và cảm nhận kết quả tiếp nha. Các này thì khá đơn giản & tận dụng được khả năng của hệ thống. Nhưng mà về bản chất cũng không khác cái Serial trên là bao nhiêu.
Lock Queue
Các giải quyết tiếp theo sẽ là sự kết hợp của Concurrent Queue và Serial Queue. Nhằm áp dụng một kĩ thuật đó là Lock Queue. Nôm na như sau:
- Concurrent Queue vẫn sẽ thực hiện các tác vụ một cách đồng thời và trên nhiều Thread.
- Tuy nhiên, đối với mỗi tác vụ nào mà chúng ta cần sự đảm bảo về mặt dữ liệu của các share data hay cả chương trình. Ta sẽ khoá Queue đó lại bằng một Serial Queue.
Đây chính là siêu năng lực của Serial Queue. Với việc thực thi đồng bộ (
.sync
) thì Serial Queue sẽ khoá Thread/Queue đó lại.
Bạn xem qua việc chỉnh sửa lại ví dụ như sau:
// Thread 1 concurrentQueue.async { self.serialQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔴 \(self.number)"] = self.number print("🔴: \(self.number)") } } } // Thread 2 concurrentQueue.async { self.serialQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔵 \(self.number)"] = self.number print("🔵: \(self.number)") } } }
Vì các tác vụ bên trong serialQueue.sync
sẽ được thực hết. Cách này giúp ta đảm bảo về mặt cấu trúc code sẽ không bị thay đổi quá nhiều. Chỉ cần thêm một Lock Queue là xong. Hãy build và cảm nhận kết quả nào.
Tuy nhiên, nếu bạn nghĩ sao về trường hợp chúng ta Lock mãi mãi. Do đó, hậu quả của việc này chính là:
DEAD LOCK
Barrier
Giải pháp cuối cùng này được xem là hoàn hảo nhất đối với chúng ta hoặc ít nhất là các bạn mới vào nghề. Ta sẽ sử dụng một rào chắn (barrier) để cản bước tiến các của tiến trình khác.
- Mọi thứ vẫn đảm bảo hoạt động đồng thời.
- Các thread và hiệu năng của hệ thống sẽ được đảm bảo và tối ưu.
- Chỉ riêng các tác vụ chỉ định với Barrier sẽ hoạt động như một Serial Queue.
Bạn xem ví dụ Barrier với Concurrent Queue của GCD sẽ như thế nào nha.
// Thread 1 concurrentQueue.async(flags: .barrier) { //self.serialQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔴 \(self.number)"] = self.number print("🔴: \(self.number)") } //} } // Thread 2 concurrentQueue.async(flags: .barrier) { //self.serialQueue.sync { for _ in 0...10 { self.number += 1 self.strings["🔵 \(self.number)"] = self.number print("🔵: \(self.number)") } //} }
Trong đó, chúng ta sẽ thêm tham số cho async()
là flags = .barrier
. Để biến việc thực thi một tác vụ nào sẽ theo kiểu tuần tự. Các tác vụ khác sẽ phải chờ nó làm xong rồi mới thực hiện tiếp.
Build và cảm nhận kết quả nha. Ahihi!
Best Practice
Sau khi đã tìm hiểu về Race Condition là gì và các giải pháp để giải quyết nó. Cũng như tránh được nó xảy ra. Sau đây, là một cách mà bạn có thể sử dụng tốt nhất các Concurrent Queue.
.sync
khi đọc dữ liệu.async
dùng để tương tác với nhiều tác khác.barrier
khi ghi dữ liệu
Okay! Tới đây, mình xin kết thúc bài viết về Race Condition tại đây. Và nếu có gì thắc mắc hay góp ý cho mình thì bạn có thể để lại bình luận hoặc gởi email theo trang Contact.
- Bài viết tiếp theo tại đây.
Cảm ơn bạn đã đọc bài viết này!
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!
1 comment
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)
Thanks ad. Bài rất hay và dễ hiểu, giải quyết được vấn đề đang gặp phải!