Contents
Chào mừng bạn đến với Fx Studio. Đây là bài viết bắt đầu cho năm 2023. Và chúng ta cùng nhau tìm hiểu chủ đề Dispatch Semaphore. Đây là một trong những chủ đề nhỏ & ít người biết tới, do nó liên quan tới việc quản lý lập trình đa luồng. Tuy nhiên, bạn thành thạo được nó sẽ giúp bạn có thêm vũ khí khá bá đạo trong việc phát triển project của mình.
Nếu mọi việc đều ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Với chủ đề này, bạn cần phải có kiến thức lập trình cơ bản nói chung và nâng cao với iOS nói riêng. Bạn cần chuẩn bị về mặt lý thuyết các chủ đều sau:
- Grand Central Dispatch – Basic Queue
- Grand Central Dispatch – Managing Task
- Race Condition và giải pháp trong 10 phút
- Thread safety & data race
Như thường lệ, đây cũng thuộc vấn đề xoắn não nên bạn cũng cần bình tĩnh và tiếp tục đọc nhóe. Chúc bạn thành công!
Vấn đề
Mình sẽ đưa ra một ví dụ đơn giản nhóe. Nhà bạn có nhiều người (bao gồm vợ chồng, ông bà, con cái) … nhưng lại có 1 cái tivi. Lúc này, nhiều người đều muốn xem kênh yêu thích của họ. Mỗi người sẽ là một luồng (thread) và cái tivi là tài nguyên dùng chung (share resource). Cách giải quyết là mỗi người xem một ít, sau đó chuyển cho người khác xem. Và ta sẽ có độ ưu tiên khác nhau với từng người trong gia đình.
Nhiệm vụ của bạn là: làm sao thoả mãn hết tất cả mọi người. Nghe đơn giản phải không nào. Ta cần phải xác định được ngữ cảnh & bản chất của vấn đề đặt ra.
- Bất đồng bộ
- Quản lý việc truy cập cùng một thời điểm cho tài nguyên dùng chung
Okay, chúng sẽ thử tìm hiểu xem việc giải quyết vấn đề này như thế nào trong các phần dưới đây. Nào tiếp tục thôi!
Semaphore là gì?
Khái niệm
Semaphore hay DispatchSemaphore cung cấp cho chúng ta khả năng quản lý việc truy cập tới các tài nguyên dùng chung (share resource) từ nhiều nguồn khác nhau (threads).
Hơi dài dòng khó hiểu phải không nào! Chúng ta đi vào phân tích thêm khái niệm nhoa. Với ngôn ngữ Swift, đây là một ngôn ngữ được thiết kế ra cho việc đa luồng (multiple threads). Do đó, việc chương trình chạy cùng lúc nhiều luồng là điều hiển nhiên.
Nếu các luồng cùng chạy nhưng không cùng nhau sử dụng một tài nguyên nào đó, thì sẽ không có chuyện gì sãy ra. Nhưng mà câu chuyện ngược lại thì cả là một vấn đề đấy.
Có thể bạn sẽ nghĩ rằng các công ngữ đã quản lý rồi, điển hình với Swift thì chúng ta có Concurrency cực kì bá đạo. Nhưng mà ta sẽ đi vào một trường hợp nhỏ hơn. Đó là:
“Việc truy cập cùng lúc, cùng một thời điểm tới tài nguyên từ nhiều nguồn khác nhau.”
Việc này sẽ gây ra việc crash chương trình. Cần lưu ý, chúng ta đang xem xét câu chuyện trong ngữ cảnh bất đồng bộ nhóe.
Cách hoạt động
Bản chất chính của DispatchSemaphore chỉ cho phép 1 thread truy cập vào tài nguyên cùng một lúc mà thôi. Do đó, cơ chế hoạt động của nó khá là giống FIFO. Ai yêu cầu trước thì sẽ thực thi trước. Cấu trúc của một Semaphore gồm có:
- Một counter để cho semaphore biết là đang có bao nhiêu thread sử dụng tài nguyên
- Một FIFO queue để theo dõi việc các thread đợi tại nguyên.
Với counter value
, đây là giá trị để quyết định thead nào sẽ truy cập được tài nguyên hay là không. Giá trị này sẽ thay đổi khi các hàm signal()
& wait()
được gọi.
Trạng thái tài nguyên
Resource Request : wait()
Khi Semaphore nhận được một request, nó sẽ kiểm tra xem counter có lớn hơn 0 hay không:
- Nếu lơn hơn, semaphore sẽ giảm sẽ giảm counter và đưa đèn xanh cho thread đó
- Ngược lại, thì nó sẽ đẩy yêu cầu sử dụng tài nguyên của thread vào hàng đợi.
Resource Release : signal()
Khi Semaphore nhận được một signal()
, nó sẽ kiểm tra xem trong FIFO queue đó có tiến trình hay luồng nào đang ở trong không:
- Nếu có, thì semaphore sẽ kéo tiến trình hoặc luồng đầu tiên từ queue vào và cho phép nó thực thi
- Ngược lại, nó sẽ tăng counter lên 1
Warning : Busy Waiting. Hãy lưu ý rằng khi bạn gọi wait()
- Semaphore sẽ đóng băng thread hiện tại lại cho tới khi nào nó được nhận đèn hiệu thực thi từ semaphore.
- Điều này có nghĩa là không được gọi
wait()
ở trên main thread và cũng thật cẩn thật khi gọi nó. - Một điều nữa là số lần
wait()
phải bằng số lầnsignal()
nếu không bạn sẽ thấy chương trình ra những bug rất oái oăm.
Bạn tham khảo đoạn code ví dụ sẽ làm đóng băng chương trình bạn nhóe.
DispatchQueue.main.async { let semaphore = DispatchSemaphore(value: 0) semaphore.wait() }
Đoạn code đó cho biết “chặn main thread đang chờ tín hiệu trên Semaphore này”. Vì vậy, cho đến khi signal()
đó đến, main thread sẽ bị chặn. Nhưng main thread sẽ không bao giờ bị chặn vì nó phục vụ cả chương trình. Nhưng trong số thức nó phục vụ đấy, giao diện người dùng và ứng dụng của bạn sẽ đóng băng nếu bạn khóa main thread.
Sử dụng Semaphore
Phần này, bạn sẽ tìm hiểu cách sử dụng nó vào từng trường hợp cụ thể. Tiếp tục nào!
Khai báo
Ta sẽ sử dụng code là ví dụ cho bạn dễ hình hung Semaphore hoạt động sao nhóe. Ta có đoạn code mẫu như sau:
let higherPriority = DispatchQueue.global(qos: .userInitiated) let lowerPriority = DispatchQueue.global(qos: .utility) func asyncPrint(queue: DispatchQueue, symbol: String) { queue.async { for i in 0...10 { print(symbol, i) } } } asyncPrint(queue: higherPriority, symbol: "🔴") asyncPrint(queue: lowerPriority, symbol: "🔵")
Thực thi đoạn code thì khá là bình thường. Trong đó:
- Ta có 2 queue là 2 thread với độ ưu tiên khác nhau
- Khi thực thi cùng một lúc, thì ai có độ ưu tiên cao hơn sẽ chạy xong trước
Áp dụng
Tới công chuyện chính nha. Lần này, ta sẽ áp dụng Semaphore xem thử nhóe! Theo dõi đoạn code sau:
let semaphore = DispatchSemaphore(value: 1) func asyncPrint(queue: DispatchQueue, symbol: String) { queue.async { print("\(symbol) waiting") semaphore.wait() // requesting the resource for i in 0...10 { print(symbol, i) } print("\(symbol) signal") semaphore.signal() // releasing the resource } } asyncPrint(queue: higherPriority, symbol: "🔴") asyncPrint(queue: lowerPriority, symbol: "🔵")
Thực thi đoạn code bạn sẽ thấy:
- Cùng gọi 2 queue một lúc, nhưng chỉ cho phép một queue chạy
- Khi queue đầu tiên chạy xong, thì queue tiếp theo mới thực thi
Bạn sẽ dễ dàng thấy được cách khai báo và sử dụng với Semaphore, vì nó chỉ là một API nhỏ trong GCD mà thôi. Trong đó:
- Khai báo:
let semaphore = DispatchSemaphore(value: 1)
- Yêu cầu:
semaphore.wait()
- Giải phóng:
semaphore.signal()
Lưu ý:
value
: sẽ cho phép số lượng luồng được chạy cùng lúc- số lần gọi
wait()
&signal()
phải bằng nhau
Xem kết quả thực thi nhoa.
Đảo ngược độ ưu tiên
Việc tiến trình có độ ưu tiên cao, mặc định sẽ được ưu tiên thực thi & truy cập tài nguyên rồi.
Không có gì phải cần thêm xử lý nữa. Nhưng vẫn là ví dụ trên, ta sẽ thay đổi thử tự thực hiện với Semaphore cho các queue có độ ưu tiên ngược lại nhóe.
asyncPrint(queue: lowerPriority, symbol: "🔵") asyncPrint(queue: higherPriority, symbol: "🔴")
Khi thực thi đoạn code bạn sẽ thấy kết quả của các tiến trình chạy không phụ thuộc vào độ ưu tiên của luồng nữa.
Điều này tốt hay xấu thì phụ thuộc vào ý đồ mà bạn muốn thực thi mà thôi. Còn về bản chất vẫn là FIFO, những tiến trình nào có signal()
trước sẽ được thực thi trước.
Việc thực hiện đảo ngược độ ưu tiên như vậy sẽ gây là một số vấn đề về tài nguyên của CPU. Vì các tiến trình đã được lập lịch và ưu tiên sẵn rồi. Do đó, việc sử dụng Semaphore nên áp dụng cho các tiến trình có cùng độ ưu tiên. Lúc này, nó mới có ý nghĩa hơn.
Counter Value
Tiếp tục, ta thử chạy ví dụ với 3 luồng và quan trọng là thay đổi số lượng counter value nhóe.
let firstQueue = DispatchQueue.global(qos: .userInteractive) let secondQueue = DispatchQueue.global(qos: .userInteractive) let thirdQueue = DispatchQueue.global(qos: .userInteractive) let semaphore = DispatchSemaphore(value: 2) func asyncPrint(queue: DispatchQueue, symbol: String) { queue.async { semaphore.wait() for i in 0...10 { print(symbol, i) } semaphore.signal() } } asyncPrint(queue: firstQueue, symbol: "🔴") asyncPrint(queue: secondQueue, symbol: "🔵") asyncPrint(queue: thirdQueue, symbol: "🟡")
Thực thi đoạn code trên bạn cũng thấy được. Mặc dù, 3 queue cùng độ ưu tiên, nhưng queue cuối sẽ chạy cuối cùng khi xét counter value là 2.
Deadlock
Deadlock chưa bao giờ là một câu chuyện đơn giản cả.
Nếu bạn chưa thành thạo xử lý việc lập trình đa luồng, thì đây là một trong những vấn đề đau đầu nhất. Cũng như các kỹ thuật hay các API liên quan tới đa luồng. Semaphore cũng xãy ra trình trạng Deadlock.
Vì mỗi Semaphore chỉ quản lý truy cập tới một tài nguyên từ 1 luồng nào đó trong 1 thời điểm. Nên việc Deadlock sẽ xãy ra khi hội tụ điều kiện:
- Sử dụng 2 Semaphore hoặc nhiều hơn
- Cùng nhau quản lý nhiều tài nguyên
- Áp dụng cho nhiều luồng cùng tương tác
Câu chuyện sẽ là:
- Thread A đang đợi tài nguyên B, thứ mà bị Thread B chiếm giữ. Chúng đang được quản lý ở Semaphore 1
- Thread B đang đợi tài nguyên A, thứ mà bị Thread A chiếm giữ. Chúng đang được quản lý ở Semaphore 2
Đọc qua chắc bạn cũng rối não ngay liên. Để xảy ra điều này ở thực tế thì cũng rất là ít. Nên tạm thời bạn chỉ cần biết sẽ có một trường hợp như vậy và ứng dụng của bạn sẽ rơi vào deadlock.
Ví dụ code tham khảo nè!
let higherPriority = DispatchQueue.global(qos: .userInitiated) let lowerPriority = DispatchQueue.global(qos: .utility) let semaphoreA = DispatchSemaphore(value: 1) let semaphoreB = DispatchSemaphore(value: 1) func asyncPrint(queue: DispatchQueue, symbol: String, firstResource: String, firstSemaphore: DispatchSemaphore, secondResource: String, secondSemaphore: DispatchSemaphore) { func requestResource(_ resource: String, with semaphore: DispatchSemaphore) { print("\(symbol) waiting resource \(resource)") semaphore.wait() // requesting the resource } queue.async { requestResource(firstResource, with: firstSemaphore) for i in 0...10 { if i == 5 { requestResource(secondResource, with: secondSemaphore) } print(symbol, i) } print("\(symbol) releasing resources") firstSemaphore.signal() // releasing first resource secondSemaphore.signal() // releasing second resource } } asyncPrint(queue: higherPriority, symbol: "🔴", firstResource: "A", firstSemaphore: semaphoreA, secondResource: "B", secondSemaphore: semaphoreB) asyncPrint(queue: lowerPriority, symbol: "🔵", firstResource: "B", firstSemaphore: semaphoreB, secondResource: "A", secondSemaphore: semaphoreA)
Thực thi đoạn code trên với Playground, thì có 2 trường hợp:
- Không bị deadlock. Vì tốc độ xử lý máy tính quá nhanh so với độ nặng của task
- Bị deadlock. Do hội tụ đủ điều kiện như mô tả ở trên
Tạm kết
- Tìm hiểu về bản chất & cách hoạt động của Semaphore
- Các trạng thái tài nguyên trong vòng đời hoạt động của nó
- Các ví dụ & trường hợp áp dụng vào coding
Tham khảo:
Okay! Tới đây, mình xin kết thúc bài viết về KeyPath trong Swift . 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.
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!
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)