Contents
Chào mừng bạn đến với Fx Studio. Chúng ta tiếp tục tìm hiểu về New Concurrency trong Swift 5.5 và đối tượng lần này là Task & Task Group. Cũng là khái niệm tiếp theo cần khám phá trong Structured Concurrency. Mục đích chính là vẫn là giúp bạn xử lý các tác vụ bất đồng bộ một cách đồng thời, nhưng sẽ có thêm yếu tố chơi theo riêng lẻ hay theo nhóm.
Nếu bạn chưa biết về Structured Concurrency thì có thể tìm hiểu bài viết dưới đây trước. Để có được cái nhìn tổng quát và các bài toán mà chúng ta cần giải quyết.
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, bạn có thể sử dụng Playground để thực thi các ví dụ. Nhưng nếu bạn sử dụng 1 iOS Project thì vẫn tốt hơn. Và chúng ta vẫn dùng console để hiển thị kết quả nhóe.
Task
Cập nhật từ Swift mới, với Structured Concurrency thì bạn sẽ được cung cấp thêm 2 kiểu mới. Đó là Task & TaskGroup. Giúp chúng ta chạy các tác vụ đồng thời theo cách riêng lẻ hoặc nhóm lại.
Ở dạng đơn giản nhất, bạn sẽ có Task. Tạo ra cho bạn một đối tượng để thực thi tác vụ mà bạn mong muốn. Ngay lập tức thì nó sẽ dược thưc thi ngày tại background. Bạn có thể sử dụng await
để gán trị hoàn thành của nó trả về cho một biến nào đó.
Cú pháp
Bạn xem qua cú pháp cơ bản của nó nhóe. Ví dụ như sau:
let simpleTask = Task { () -> String in return "a simple task" }
Bạn sẽ thấy cú pháp tạo ra một Task sẽ bao gồm các tham số sau:
Task(priority: <TaskPriority?>, operation: <() async -> _>)
-
Cung cấp độ ưu tiên thực thi cho tham số
priority
-
operation
là công việc cần được thi thực.
Mặc định, thì công việc của bạn sẽ là function với async nhóe. Có thể có giá trị trả về hoặc không. Với ví dụ trên, chúng ta sử dụng kiểu String trả về cho operation
. Thực thi Task thì sẽ như thế này:
Task { print(await simpleTask.value) }
Đơn giản là bạn tiếp tục tạo thêm 1 Task với kiểu Void để sử dụng kết quả của simpleTask
. Hoặc bạn có thể sử dụng trong một function bất đồng bộ nào đó cũng được.
func doSomething() async { print("Begin") print(await simpleTask.value) print("End") } // thực thi Task { await doSomething() }
Quá EZ phải không nào!
Áp dụng
Tất nhiên, bạn sẽ áp dụng nó vào một bài toán cụ thể, thì mới thấy được công dụng của nó nhiều hơn. Chúng ta lấy ví dụ của việc in ra 50 số Fibonacy đầu tiên nhóe. Bạn sẽ có function tìm một số fibonacy thứ n như sau:
func fibonacci(of number: Int) -> Int { var first = 0 var second = 1 for _ in 0..<number { let previous = first first = second second = previous + first } return first } print(fibonacci(of: 15)) // số fibo thứ 15 là 610
Function sẽ lặp từ 0 tới number lần, để xác định được số fibo thứ number đó. Có nghĩa bạn đang có 1 for
để làm việc ấy. Và bạn sẽ nâng cấp độ khó của bài toán n lần khi in ra dãy 50 số fibo đầu tiên nhóe. Có nghĩa lần này là for lồng for.
func printFibonacciSequence() { var numbers = [Int]() for i in 0..<50 { let result = fibonacci(of: i) numbers.append(result) } print("🔵 The first 50 numbers in the Fibonacci sequence are: \(numbers)") } printFibonacciSequence()
Điều này sẽ rất tốn tài nguyên của hệ thống. Vì tất cả công việc sẽ ném vào 1 thread và thưc thi lần lượt. Chúng ta sẽ giải quyết chúng bằng Task và cho chúng chạy đồng thời tại backgroud nhóe. Xem tiếp ví dụ nào!
func printFibonacciSequence2() async { let task1 = Task { () -> [Int] in var numbers = [Int]() for i in 0..<50 { let result = fibonacci(of: i) numbers.append(result) } return numbers } let result1 = await task1.value print("⚪️ The first 50 numbers in the Fibonacci sequence are: \(result1)") } Task { await printFibonacciSequence2() }
Ví dụ trên các task tìm số fibo thứ n sẽ được chạy đồng thời với nhau. Tác vụ bắt đầu chạy ngay sau khi nó được tạo và hàm printFibonacciSequence()
sẽ tiếp tục chạy trên bất kỳ chuỗi nào trong khi các số Fibonacci đang được tính toán.
Ví dụ cao cấp hơn nhóe:
let task1 = Task { (0..<50).map(fibonacci) } Task { print("🔴 The first 50 numbers in the Fibonacci sequence are: ") print(await task1.value) }
Bạn chú ý các hình tròn màu mà mình để trong các lệnh print nhóe. Chúng sẽ thể thứ tự thực thi các task lớn.:
- Màu xanh là đồng bộ. Các task khác sẽ chờ nó làm xong
- Màu đỏ sẽ hoàn thành trước vì cấp độ nó ngang cấp với màu xanh
- Màu xanh thì sẽ tạo ra các child task bên trong nó. Nó sẽ hoàn thành khi tất cả các child task hoàn thành.
Task priority
Khi tạo 1 task thì bạn sẽ được cung cấp các mức ưu tiên priority
như sau:
- high
- default
- low
- background
Mặc định khi bạn không cấp cho tham số priority
, thì nó sẽ nhận là default. Đối chiếu sang hệ thống thì bạn sẽ có quy ước như sau:
- userInitiated = high
- utility = low
- Bạn không thể truy cập userInteractive, vì nó là main thread
Static methods
Chúng ta có các phương thức tĩnh của Task, giúp bạn điều kiển các Task thuận lợi hơn.
- Task.sleep() task hiện tại sẽ sang chế độ ngủ. Đơn vị thời gian là nano giây. Nghĩa là
1_000_000_000 = 1 giây
- Task.checkCancellation() kiểm tra xem ai đó có gỏi nó tự hũy hay không, bằng phương thức
cancel()
. Lúc ấy, sẽ ném về 1 giá trị CancellationError - Task.yield() dừng task hiện tại trong một thời gian. Để dành cho task khác đang chờ. Có ích trong các vòng for không lối thoát.
Ví dụ tổng hợp nhóe!
func cancelSleepingTask() async { let task = Task { () -> String in print("Starting") try await Task.sleep(nanoseconds: 1_000_000_000) try Task.checkCancellation() return "Done" } // The task has started, but we'll cancel it while it sleeps task.cancel() do { let result = try await task.value print("Result: \(result)") } catch { print("Task was cancelled.") } } Task { await cancelSleepingTask() }
Trong đó:
- Ngay khi task bắt đầu thì rơi vào lệnh ngủ 1 giây
- Nhưng đã bị gọi cancel từ bên ngoài
- Ở trong closure có kiểm tra việc có bị hũy hay không. Lúc này nó sẽ throw lỗi về tại do catch
Và khi lệnh hũy được đưa ra, thì giá trị value sẽ không được gởi về. Và để lấy được giá trị khi hũy vẫn diễn ra, thì bạn hay sử dụng tới task.result
. Đó là 1 kiểu Result<String, Error> (theo như ví dụ trên). Công việc chỉ còn là switch … case mà thôi.
Task Group
Đối với nhiều công việc phức tạp, thì việc sử dụng các Task riêng lẻ, chúng sẽ không được hiệu quả cao nhất. Lúc này, bạn cần sử dụng tới Task Group. Nó sẽ tập hợp các nhiệm vụ (task) để thực hiện cùng nhau nhằm tạo ra 1 giá trị khi hoàn thành.
Task Group sẽ hạn chế nhỏ nhất rủi ro mà bạn sẽ gặp phải.
Cú pháp
Bạn không thể tạo ra Task Group một cách trực tiếp. Sử dụng function withTaskGroup()
để tạo với nội dung công việc bạn muốn hoàn thành. Và bạn sẽ có 2 cách để tạo:
- withThrowingTaskGroup
- withTaskGroup
Chúng tương tự nhau chỉ khác nhau ở có throw và không mà thôi. Các tác vụ con sẽ được thêm vào Task Group thông qua phương thức addTask()
. Các task con được thêm vào sẽ được thực thi ngay.
Áp dụng
Xem qua ví nhóe!
func printMessage() async { let string = await withTaskGroup(of: String.self) { group -> String in group.addTask { "Hello" } group.addTask { "From" } group.addTask { "A" } group.addTask { "Task" } group.addTask { "Group" } var collected = [String]() for await value in group { collected.append(value) } return collected.joined(separator: " ") } print(string) } // thực thi Task { await printMessage() }
Trong đó:
string
là biến tạo ra để nhận giá trị cuối cùng task group sau khi hoàn thành.- Việc thêm các task con trong group thông qua phương thức group.addTask { }
- Kiểu giá trị trả về của Task Group và Task con thường sẽ giống nhau.
- Các giá trị của các task con mà bạn muốn lấy thì sẽ phải chờ await
- Task group sẽ trả về giá trị sau khi tất cả các task con đã hoàn thành
Bạn sẽ hiểu mỗi Task con như là một function vậy. Tuy nhiên, chúng sẽ tự động thực thi & trong khi đó Task Group sẽ đợi tất cả hoàn thành trước khi trả về giá trị. Dẫn tới một điều rằng:
Đôi khi thứ tự trả về của các task con sẽ không như mong muốn.
With Error
Trường hợp, Task Group của bạn đang thực thi mà có lỗi phát sinh trong các task con, thì sẽ như thế nào? Bạn cần phải thiết kế lại việc tạo Task Group, lần này bạn sẽ dùng function withThrowingTaskGroup()
để tạo Task Group. Đi kèm với đó là bạn sẽ cần sử dụng thêm try
trước await. Vì có thể sinh ra lỗi trong quá trình thực thi.
Xem ví dụ nhóe!
enum LocationError: Error { case unknown } func getWeatherReadings(for location: String) async throws -> [Double] { switch location { case "London": return (1...100).map { _ in Double.random(in: 6...26) } case "Rome": return (1...100).map { _ in Double.random(in: 10...32) } case "San Francisco": return (1...100).map { _ in Double.random(in: 12...20) } default: throw LocationError.unknown } } func printAllWeatherReadings() async { do { print("Calculating average weather…") let result = try await withThrowingTaskGroup(of: [Double].self) { group -> String in group.addTask { try await getWeatherReadings(for: "London") } group.addTask { try await getWeatherReadings(for: "Rome") } group.addTask { try await getWeatherReadings(for: "San Francisco") } // Convert our array of arrays into a single array of doubles let allValues = try await group.reduce([], +) // Calculate the mean average of all our doubles let average = allValues.reduce(0, +) / Double(allValues.count) return "Overall average temperature is \(average)" } print("Done! \(result)") } catch { print("Error calculating data.") } } // thực thi Task { await printAllWeatherReadings() }
Trong ví du:
- Các lệnh group.addTask hầu như giống nhau. Nên bạn có thể nhóm lại bằng 1 vòng lặp
- Giá trị sẽ được làm gọn từ nhiều array double thành 1 array double, bằng toán tử reduce
- Cuối cùng là tính giá trị trung bình của chúng
Tiếp theo, bạn đặt thử 1 thành phố không có trong dữ liệu vào Task Group và quan sát kết quả nhóe!
group.addTask { try await getWeatherReadings(for: "Hanoi") }
Lúc này, Task Group sẽ gọi cancelAll()
để hũy bất kỳ task còn nào trong nó. Nhưng các lệnh addTask vẫn sẽ được thực thi. Điều này gây ra sự tốn kém tài nguyên. Các khắc phục thì bạn sẽ sử dụng hàm thay thế addTaskUnlessCancelled()
. Nó sẽ dừng việc thêm Task con khi Group phát lệnh hũy.
Tạm kết
Ở trên, mình chỉ giới thiệu lại cơ bản của Task & Task Group trong Swift 5.5 mà thôi. Còn việc áp dụng của nó tùy thuộc vào bạn nắm được bao nhiêu kiến thức của New Concurrency trong Swift mới. Còn các khái niệm Concurrency mới có sự liên hệ chặt chẽ với nhau. Và hỗ trợ nhau rất nhiều. Mình sẽ trình bày ở các bài viết khác.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Task & Task Group 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
- CO-STAR – Công thức vàng để viết Prompt hiệu quả cho LLM
- Prompt Engineering trong 10 phút
- Một số ví dụ sử dụng Prompt cơ bản khi làm việc với AI
- Prompt trong 10 phút
- 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
You may also like:
Archives
- 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)