Skip to content
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Fx Studio
  • Home
  • Code
  • iOS & Swift
  • Combine
  • RxSwift
  • SwiftUI
  • Flutter & Dart
  • Tutorials
  • Art
  • Blog
Race Condition
Written by chuotfx on June 28, 2021

Race Condition và giải pháp trong 10 phút – Swift

iOS & Swift . Tutorials

Contents

  • Chuẩn bị
  • Race Condition là gì?
    • Định nghĩa
    • Nguyên nhân
  • Ví dụ
    • Ví dụ #1
    • Ví dụ #2
  • Giải pháp
    • Serial Queue
    • Synchronous
    • Lock Queue
    • Barrier
  • Best Practice

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ơ bản về async/await trong 10 phút – Swift 5.5
    • async/await to Fetch REST API – Swift 5.5

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:

    • Grand Central Dispatch – Basic Queue
    • Grand Central Dispatch – Managing Task

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.

Race Condition

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ành 18

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

Race Condition

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ằng sync
// 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.

Race Condition

  • 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!

FacebookTweetPinYummlyLinkedInPrintEmailShares19

Related Posts:

  • Nimble
    Nimble trong 10 phút
  • POP
    Lập trình hướng giao thức (POP) với Swift
  • feature_bg_swift_04
    Complete Concurrency với Swift 6
  • KeyPath
    KeyPath trong 10 phút - Swift
Tags: concurrency, Swift
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

  • Hai Nguyen H.P has written: June 28, 2021 at 7:21 am Reply

    Thanks ad. Bài rất hay và dễ hiểu, giải quyết được vấn đề đang gặp phải!

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Donate – Buy me a coffee!

Fan page

Fx Studio

Tags

Actor Advanced Swift AI api AppDistribution autolayout basic ios tutorial blog ci/cd closure collectionview combine concurrency crashlytics dart dart basic dart tour Declarative delegate deploy design pattern fabric fastlane firebase flavor flutter GCD gradients iOS MVVM optional Prompt engineering protocol Python rxswift safearea Swift Swift 5.5 SwiftData SwiftUI SwiftUI Notes tableview testing TravisCI unittest

Recent Posts

  • [Swift 6.2] Raw Identifiers – Đặt tên hàm có dấu cách, tại sao không?
  • Vibe Coding là gì?
  • Cách Đọc Sách Lập Trình Nhanh và Hiệu Quả Bằng GEN AI
  • Nỗ Lực – Hành Trình Kiến Tạo Ý Nghĩa Cuộc Sống
  • Ai Sẽ Là Người Fix Bug Khi AI Thống Trị Lập Trình?
  • Thời Đại Của “Dev Tay To” Đã Qua Chưa?
  • Prompt Engineering – Con Đường Để Trở Thành Một Nghề Nghiệp
  • Vấn đề Ảo Giác (hallucination) khi tương tác với Gen AI và cách khắc phục nó qua Prompt
  • Điều Gì Xảy Ra Nếu… Những Người Dệt Mã Trở Thành Những Người Bảo Vệ Cuối Cùng Của Sự Sáng Tạo?
  • Khi Cô Đơn Gặp Python

You may also like:

  • Dart Defines trong Flutter và sức mạnh của nó
    Dart Defines
  • Nimble trong 10 phút
    Nimble
  • Dispatch Semaphore trong 10 phút
    Semaphore
  • [Swift 6.2] Raw Identifiers - Đặt tên hàm có dấu…
    feature_bg_swift_04
  • Lập trình hướng giao thức (POP) với Swift
    POP

Archives

  • May 2025 (1)
  • April 2025 (1)
  • March 2025 (8)
  • January 2025 (7)
  • December 2024 (4)
  • 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)

About me

Education, Mini Game, Digital Art & Life of coders
Contacts:
contacts@fxstudio.dev

Fx Studio

  • Home
  • About me
  • Contact us
  • Mail
  • Privacy Policy
  • Donate
  • Sitemap

Categories

  • Art (1)
  • Blog (43)
  • Code (11)
  • Combine (22)
  • Flutter & Dart (24)
  • iOS & Swift (102)
  • No Category (1)
  • RxSwift (37)
  • SwiftUI (80)
  • Tutorials (86)

Newsletter

Stay up to date with our latest news and posts.
Loading

    Copyright © 2025 Fx Studio - All rights reserved.