![structured concurrency](https://fxstudio.dev/wp-content/uploads/2021/11/feature_bg_aysnc_let-1170x628.jpg)
Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã lâu rồi mới quay lại về nội dung thuần túy với Swift. Chủ đề của bài về này sẽ là Structured Concurrency & async let trong Swift 5.5 . Và cách chúng ta sẽ giải quyết các bài toán thực hiện đồng thời (concurrency) các tác vụ bất đồng bộ (async) trong Swift.
Để bạn vào bài dễ dàng hơn, thì cần phải biết được khái niệm async/await trong Swift 5.5. Vì đây là nền tảng cơ bản đầu tiên của New Concurrency trong Swift. Nếu bạn chưa đọc hoặc chưa biết qua nó, thì có thể tham khảo link dưới đây.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Tất nhiên, với khái niệm mới này thì bạn cần chuẩn bị Swift của mình là mới nhất. Sau đây là version cho các tool của bạn.
-
- Swift 5.5
- macOS 12.0.x
- Xcode 13.1
Đó là một tổ hợp các tool và môi trường phù hợp. Mình đã phải chờ rất lâu từ bản beta cho Swift 5.5 và Xcode 13 tới bản chính thức hiện tại này. Thì mới tiếp tục được Structured Concurrency & series New Concurrency trong Swift.
Về mặt kiến thức, bạn cần chuẩn bị kha khá kiến thức. Nhất là về bất đồng bộ. Bạn có thể tham khảo các bài viết về bất đồng bộ trên website này tại link dưới đây:
Về mặt demo, do vẫn còn hạn chế trong nền tảng mới. Bạn không thể sử dụng Playground, thay vào đó bạn hãy tạo 1 iOS Project mới và hiển thị kết quả ở console nhóe.
Structured Concurrency
async/await
Bạn cũng thấy chúng ta đã được Swift cung cấp cho một tính năng khá là hay. Với Swift 5.5 chúng ta có async/await, mọi việc với bất đồng bộ hầu như đơn giản đi rất nhiều. Bạn sẽ thoát khỏi cái cảnh đâu đầu với đám call back hay delegate mỗi lần làm việc với bất đồng bộ.
Tuy nhiên, cái gì cũng có 2 mặt của nó. Và với async/await thì bạn hầu như thực hiện công việc một cách tuyến tính. Có nghĩa là:
- Code chạy từ trên xuống dưới
- Các câu lệnh thực hiện tuần tự và dừng chờ lần nhau
- Không thể thực hiện bất đồng bộ một lúc nhiều tác vụ được
Structured Concurrency
Đó chính là điểm hạn chế nhất của async/await trong Swift. Và Apple không bao giờ bỏ rơi chúng ta. Bạn sẽ khai phá tiếp một khái niệm nữa trong Swift. Đó là Structured Concurrency.
Ý tưởng của Structured Concurrency cũng được xây dựng trên những structured programming. Bạn không cần phải quá lo lắng nhiều về chúng. Vẫn là những thứ quen thuộc với bạn, nhưng bây giờ bạn đặt thêm tiêu chí thời gian vào trong đó.
- Các function hay các giá trị của các thuộc tính … sẽ được hoạt động đồng thời & xác định dựa trên ngữ cảnh mà bạn đặt nó vào.
- Các biến/thuộc tính vẫn được khai báo và xác định phạm vi của nó.
- Về các callback concurrency sẽ được định nghĩa ở các thread khác nhau hoặc các ngữ cảnh khác nhau trong khi main vẫn đang thực thi.
Nghe cũng hơi nỗ não nhĩ!
Tóm tắt đơn giản lại thì thế này:
- async/await bạn sẽ thực hiện các tác vụ bất đồng bộ nhưng chỉ được 1 task và phải dừng chờ
- Structured Concurrency bạn sẽ thưc hiện được nhiều tác vụ đồng thời mà không cần quan tâm tới việc các task sẽ dừng chờ lẫn nhau.
Bài toàn lập trình
Chúng ta sẽ có một ví dụ đơn giản như sau:
enum MyError: Error { case anError } struct Student { var name: String var classes: [String] var scores: [Int] } func getStudentName() async -> String { await Task.sleep(1_000_000_000) return "Fx Studio" } func getClasses() async -> [String] { await Task.sleep(1_000_000_000) return ["A", "B", "C", "D", "E", "F"] } func getScores() async -> [Int] { await Task.sleep(1_000_000_000) return [10, 9, 7, 8, 9, 9] }
Trong đó:
- Khai báo 2 cấu trúc dữ liệu mới là Student và MyError
- Thêm 3 function bất đồng bộ (async) để trả dữ liệu về
Còn về Task.sleep(1_000_000_000)
thì bạn chỉ cần hiểu là dừng tác vụ đó trong 1 thời gian. Đơn vị của nó nano giây, nên 1_000_000_000 = 1 giây
. Cũng hơi tốn kém nhĩ!
Mọi việc vẫn không có gì sai, tuy nhiên bạn hãy xem tiếp 1 function nữa sau đây.
func printStudentInfo() async { print("\(Date()): get name") let name = await getStudentName() print("\(Date()): get classes") let classes = await getClasses() print("\(Date()): get score") let scores = await getScores() print("\(Date()): Creating ....") let student = Student(name: name, classes: classes, scores: scores) print("\(Date()): \(student.name) - \(student.classes) - \(student.scores)") }
Bạn sẽ kết hợp các function async trên vào printStudentInfo()
. Thử thực thi nó xem như thế nào.
Task { await printStudentInfo() }
Bạn sử dụng
Task { ... }
thay choasync { ... }
nhóe, vì Swift mới lại cập nhật mới nữa.
Và quan sát kết quả ở console thì như thế này:
2021-11-04 07:51:07 +0000: get name 2021-11-04 07:51:08 +0000: get classes 2021-11-04 07:51:09 +0000: get score 2021-11-04 07:51:10 +0000: Creating .... 2021-11-04 07:51:10 +0000: Fx Studio - ["A", "B", "C", "D", "E", "F"] - [10, 9, 7, 8, 9, 9]
Nhưng có một điều hơi sai sai ở đây, chính là cách chúng ta định nghĩa function printStudentInfo
. Bạn sẽ dễ dàng nhận ra rằng:
- Các lệnh được khai báo kèm với await
- Nghĩa ra chúng sẽ chờ nhau, lên sau phải đợi lệnh trước hoàn thành rồi mới thực thi
Mong muốn của bạn là bất đồng bộ cho 3 task name, classes & scores đã thất bại.
Sẽ càng thêm khó nếu xảy ra các trường hợp như thế này:
- 1 trong 3 task bị fail thì như thế nào
- bao lâu để hoàn thành hết
Vấn đề này, bạn có thể dùng DispatchGroup để giải quyết nó. Nhưng đó là cách Concurrency cũ. Chúng ta sẽ gỡ rối với cách mới nhóe! Và Swift 5.5, sẽ cho bạn 2 cách giải quyết vấn đề này với Structured Concurrency:
- async let
- Task group
(Trong phạm vi bài viết thì mình xin phép đề cập tới mỗi async let, với Task Group thì hẹn bài viết sau.)
Task
Khái niệm
Đây là khái niệm mới được đưa vào Swift và New Concurrency của nó. Task được xem là một đơn vị nhỏ nhất để định nghĩa 1 tác vụ mà bạn mong muốn chúng sẽ được thực hiện một cách song song. Một Task sẽ ở trong một ngữ cảnh async, tại đó nó được thưc thi đồng thời cùng với các Task khác.
Dễ hiểu hơn là chúng sẽ ở trong các function async. Ahihi!
Còn về ví dụ trên của chúng ta, function printStudentInfo
sẽ không thực sự có task nào được tạo ra. Vì bạn đã sử dụng await. Nghĩa là dừng chờ và tuần tự mà thôi, không chạy song song được.
Đặc tính
Về Task đã được đưa vào Swift từ trước, nó nằm sâu trong đó. Bạn không bao giờ thấy được. Hầu như chúng đều tự động được gọi tới. Và trình biên dịch sẽ giúp bạn hạn chế các lỗi khi viết mã bất đồng bộ. Nhưng với bất đồng bộ thì không bao giờ đơn giản cả. Bạn cần xem xét ngữ cảnh hiện tại để thực hiện các Task của bạn cho hợp lý.
Ta có thể tóm tắt:
- Function với async là function bất đồng bộ, nhưng không có nghĩa là một task được tạo ra
- Trình biên dịch sẽ đánh dấu function async đó và sẽ chờ await mỗi khi nó được gọi
- Các Task sẽ không được tạo ra một cách tự động.
- Với Task thì bạn cho trình biên dịch biết đoạn mã đó sẽ được thực hiện đồng thời
- Task luôn được tạo ra với một nhiệm vụ rõ ràng & cụ thể
Structured concurrency is about a balance between simplicity and flexibility.
Đây là cách mà Apple muốn bạn chú ý. Bạn sẽ làm được nhiều công việc với Concrrency, nhưng chúng không phải là tất cả. Và chúng ta phải chấp nhận một số sự ràng buộc. Khi bạn muốn sự linh hoạt thì sẽ phải sử dụng tới các API low level, nhưng lại không an toàn. Cân bằng giữa đơn giản & linh hoạt mới là điều quan trọng nhất. Thành bại vẫn do bạn quyết định.
async let task
Với async let là một cách nhanh nhất và đơn giản nhất để bạn có được 1 Task.
Hay còn có một tên gọi khác là concurrent binding.
Bạn thử xem xét ví dụ sau:
async let thing = something()
Trong đó:
something()
sẽ được thực thi trước và kết quả sẽ gán vềlet thing
. Với cách truyền thống là như vậy.- Với async let thì
something()
sẽ là async & được thực thi trước tiên. Sau đó sẽ gán cholet thing
Khi bạn thực thi lệnh async let thì một task được tạo và nó gọi là child task. Các tác vụ cha sẽ vẫn tiếp tục chạy cho đến một lúc nào đó mà chúng ta cần kết quả của thing
đó.
async let thing = something() // some stuff makeUseOf(await thing)
Các lệnh sau async let sẽ được thực thi mà không quan tâm tới việc dừng chờ. Như vậy, bạn đã hình dùng qua được khái niệm async let rồi đó nhĩ. Chúng ta sẽ áp dụng nó vào ví dụ trên thôi. Bạn tham khảo đoạn code sau:
func printStudentInfo2() async { print("\(Date()): get name") async let name = getStudentName() print("\(Date()): get classes") async let classes = getClasses() print("\(Date()): get scores") async let scores = getScores() print("\(Date()): Creating ....") let student = await Student(name: name, classes: classes, scores: scores) print("\(Date()): \(student.name) - \(student.classes) - \(student.scores)") }
Mình đã cải tiến lại function trên với việc khai báo thêm các async let. Khi bạn thực thi function và sẽ cảm nhận kết quả được ở console rất là khác biệt.
2021-11-04 08:12:51 +0000: get name 2021-11-04 08:12:51 +0000: get classes 2021-11-04 08:12:51 +0000: get scores 2021-11-04 08:12:51 +0000: Creating .... 2021-11-04 08:12:52 +0000: Fx Studio - ["A", "B", "C", "D", "E", "F"] - [10, 9, 7, 8, 9, 9]
Chúng ta sẽ mất 2 giây cho toàn bộ, có nghĩa 3 task lấy thông tin đã hoạt động đồng thời. EZ Game!
The Task Tree
Ta sẽ lấy một ví dụ tiếp theo với 2 task:
func getClassesAndScores() async -> ([String], [Int]) { async let classes = getClasses() async let scores = getScores() return await (classes, scores) }
Bạn sẽ có 1 function mới với 2 Task con được định nghĩa bằng async let. Bạn thử suy nghĩ tiếp là ta sẽ đặt tiếp function getClassesAndScores
vào trong 1 function khác thì sẽ như thế nào nha. Xem ví dụ tiếp nào.
func printStudentInfo3() async { print("\(Date()): get name") async let name = getStudentName() print("\(Date()): get classes & scores") async let results = getClassesAndScores() print("\(Date()): Creating ....") let student = await Student(name: name, classes: results.0, scores: results.1) print("\(Date()): \(student.name) - \(student.classes) - \(student.scores)") }
Mình tiếp tục sử dụng async let cho function getClassesAndScores
. Biến nó thành 1 Task con. Như vậy, bạn có tới 3 cấp rồi.
- printStudentInfo3 là cấp thứ nhất
- async let name & async let results là cấp thứ hai
- async let classes & async let scores là cấp thứ 3
Tuy với nhiều cấp được tạo ra như vậy, nhưng không có tác vụ nào dừng chờ cả. Tất cả đều được thực hiện đồng thời. Tác vụ cha sẽ tạo 1 hoặc nhiều task con để thưc thi công việc của mỗi cái.
- Nếu 1 trong chúng gặp lỗi, thì task cha sẽ thoát.
- Nếu lúc đó vẫn còn nhiều tack con đang chạy, thì task cha sẽ đánh dấu và báo kết thúc trước khi thoát.
- 1 task con bị đánh dấu hủy thì các task con của task con cũng sẽ tự động hủy
- Task cha hoàn thành khi tất cả các task con trong đó hoàn thành
Tổng hợp lại, bạn sẽ cảm nhận chúng như là cách nhánh của một cái cây. Có quan hệ với nhau tại một đầu mối. Điều này đảm bảo luôn hoàn thành tất cả các tác vụ (thành công, thông qua việc hủy bỏ hoặc bằng cách tạo ra lỗi) là điều cơ bản đối với tính đồng thời trong Swift.
Cancellation
Error
Chúng ta sẽ làm các ví dụ mới, để xem cho việc một Task bị hủy thì các task khác sẽ như thế nào.
func getStudentName2() async throws -> String { await Task.sleep(1_000_000_000) return "Fx Studio" } func getClasses2() async throws -> [String] { throw MyError.anError //await Task.sleep(1_000_000_000) //return ["A", "B", "C", "D", "E", "F"] } func getScores2() async throws -> [Int] { await Task.sleep(1_000_000_000) return [10, 9, 7, 8, 9, 9] }
Trong đó, function getClasses2
sẽ nén về mộterror
thay vì một giá trị. Chúng ta tiếp tục xem ví dụ cho function kết hợp nhóe.
func printStudentInfo4() async { do { print("\(Date()): get name") async let name = getStudentName2() print("\(Date()): get classes") async let classes = getClasses2() print("\(Date()): get scores") async let scores = getScores2() print("\(Date()): Creating ....") let student = try await Student(name: name, classes: classes, scores: scores) print("\(Date()): \(student.name) - \(student.classes) - \(student.scores)") } catch { print("Error: \(error)") } }
Vì có throw
nên chúng ta cần do try catch
để bắt lỗi. Bạn hãy thức thi function đó xem kết quả như thế nào.
2021-11-04 08:26:37 +0000: get name 2021-11-04 08:26:37 +0000: get classes 2021-11-04 08:26:37 +0000: get scores 2021-11-04 08:26:37 +0000: Creating .... Error: anError
Thay vì nhận được giá trị, chúng ta nhận về một anError
mà thôi. Và khi một task thất bại. Swift sẽ đánh dấu các task con của nó là cancelled. Điều này có nghĩa sẽ bị hủy chứ không phải bị hủy.
Nghĩa là kết quả của nó sẽ không trả về mà thôi.
Check Error
Lại sinh ra một vấn đề là: bạn muốn chúng thực sự bị hủy đi, khi task cha của nó đã báo và đánh dấu hủy rồi. Nhằm giảm bớt đi sự làm việc của hệ thống. Tiết kiệm tài nguyên nhiều hơn.
Bạn lại có 2 cách kiểm tra việc Task có đang bị hủy hay không:
try Task.checkCancellation()
khi ai đó đã đánh dấu bạn hủy, và throw lại một CancellationErrorTask.isCancelled
trả về một bool cho các function khôngthrow
Ta sẽ cập nhật lại ví dụ một tí nhóe.
func getScores2() async throws -> [Int] { await Task.sleep(1_000_000_000) if Task.isCancelled { return [10, 9, 7, 8, 9, 9] } else { print("getScores2 will cancel") throw MyError.anError } }
hoặc như thế này nữa
func getScores2() async throws -> [Int] { try Task.checkCancellation() await Task.sleep(1_000_000_000) print("getScores2 calling") return [10, 9, 7, 8, 9, 9] }
Đó là 2 cách mà bạn sẽ kiểm tra được Task cha có đang hủy hay là không. Rồi từ đó bạn sẽ có những cách xử lý phù hợp. Hãy thực thi lại ví dụ và cảm nhận kết quả nha.
Tạm kết
- Tìm hiểu về khái niệm Structured Concurrency trong Swift 5.5
- Khái niệm về Task trong bất đồng bộ và cách một Task hoạt động
- Cách thực thi các task bất đồng bộ một cách đồng thời với async let
- Xử lý trường hợp khi gặp lỗi trong quá trình thực thi Task
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Structured Concurrency & async let trong Swift 5.5 . 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
- 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
- Lập trình hướng giao thức (POP) với Swift
You may also like:
Archives
- 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)