Contents
Chào mừng bạn đến với Fx Studio. Chúng ta đã có một hành trình đẹp với các thực thể mới trong New Concurrency của Swift 5.5. Bạn đã tiếp cận với nhiều khái niệm của Contructured Concurrency, nhưng bạn thử suy nghĩ Không cấu trúc (Unstructured Concurrency) thì sẽ như thế nào. Bạn sẽ được cung cấp thêm một khái niệm mới là Detached Tasks, sẽ đại diện cho việc tương tác đồng thời không cấu trúc. Và chúng ta sẽ tìm hiểu nó ở bài viết này.
Nếu bạn chưa biết gì về Contructured Concurrency, thì có thể tìm hiểu thêm ở các link dưới đây.
Còn nếu mọi việc đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Chúng ta lại tiếp cận một khái niệm mới trong Swift 5.5, do đó bạn sẽ cần chuẩn bị các môi trường & tools là mới nhất. Bạn tham khảo nhóe!
-
- Swift 5.5
- macOS 12.0.x
- Xcode 13.1
Về mặt demo, bạn cần chuẩn bị một iOS Project để đảm báo có được Main Thread và UI cho các ví dụ được dùng trong bài viết. Giao diện sử dụng cho demo thì chúng ta sẽ không cần dùng tới, mọi thứ hầu hết sẽ sử dụng console để in kết quả.
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:
Detached Tasks
Khái niệm
Khi bạn nói tới Contructured Concurrency, thì đồng nghĩa với đó bạn sẽ sử dụng tới các Task & TaskGroup hay các async let. Chúng tạo nên các cấu trúc, task cha & task con … từ đó tạo nên tính liền mạch trong việc thực thi các tác vụ đồng thời trong chương trình. Cũng là sự chờ đợi và phụ thuộc lẫn nhau. Đây cũng là điều hạn chế của Contructured Concurrency.
Do đó, Swift 5.5 lại cung cấp thêm 1 khái niệm nữa để khắc phục nhược điểm trên. Dó là, Detached Task. Nó rất hữu ích khi bạn cần thực hiện một tác vụ hoàn toàn độc lập với tác vụ cha. Nó độc lập trong ngữ cảnh hiện tại mà nó được khởi tạo & thực thi.
Đặc tính
Nó sẽ mang các đặc điểm sau:
- Có thể khởi tạo và thực thi ở bất cứ đâu.
- Thời gian tồn tại của của chúng không có phạm vi
- Không bị ảnh hưởng bởi các task hiện tại
- Không chịu quản lý bởi thread hiện tại
- Cẩn thận khi sử dụng trong cách Actor
- Không kế thừa lại bất cứ tác vụ nào của các Task cha/mẹ
- …
Còn nói dễ hiểu thì:
Detached Task sẽ tách tác vụ của nó ra khỏi các Task hiện tại, kể cả Thread hiện tại và không thuộc vùng isolated của Actor quản lý nó.
Bạn có vẫn có thể quản lý nó thông qua các Task.Handle, như: Canncel … xem nó như một task lớn nhất (trong cấu trúc của bạn).
Ví dụ
Đọc lý thuyết thì nhiều quá cũng hơi loạn não nhĩ. Giờ thì chúng ta sẽ đi vào ví dụ đơn giản trước nhóe.
Đầu tiên, chúng ta có các function cơ bản như sau:
func getName() async -> String { print("--> getName") return "Fx Studio" } func getFriends() async -> Int { print("--> getFriends") return 9999 } func saveInfor(name: String, friends: Int) async { print("Saving: \(name) - \(friends) ...") }
Ý nghĩa các function bất đồng bộ này thì như đúng tên của chúng rồi. Bạn hãy tưởng tượng rằng getName
& getFriends
là sẽ sử dụng API để lấy thông tin về. Còn function saveInfor
thì sẽ lưu lại thông tin vào Database, nó sẽ thực hiện độc lập với các tiến trình còn lại.
Ta tiếp tục với function thực thi tác vụ lớn, bọc 3 tác vụ trên nhóe.
func getInfo() async -> (String, Int) { let name = await getName() let friends = await getFriends() Task.detached { await saveInfor(name: name, friends: friends) } let infor = (name, friends) print("return ...") return infor }
Trong đó:
getInfor
cũng là một function bất đồng bộ- Bạn sẽ cần thời gian và chờ đợi
await
để lấy các thông tin cần thiết. Nếu bạn muốn chúng hoạt động đồng thời, thì có thể dùng TaskGroup tại đây nhóe. - Với tác vụ
saveInfor
, ta sử dụng Task.detached và cho chạy riêng ở.background
ngay sau khi bạn lấy được thông tin. - Song song lúc đó, ta sẽ trả giá trị
infor
về lại nơi gọigetInfor
thực thi
Thực thi chương trình như sau:
Task { let infor = await getInfo() print("Infor: \(infor)") }
Bạn sẽ thấy được kết quả như sau:
--> getName --> getFriends return ... Infor: ("Fx Studio", 9999) Saving: Fx Studio - 9999 ...
Vì độ ưu tiên của saveInfor
thấp hơn, mặc dù nó được khởi chạy trong Task cha. Tuy nhiên, với Detached Task thì nó được thực thi ở một ngữ cảnh riêng. Do đó, task cha của bạn vẫn hoàn thành và không phải chờ đợi Detached Task đó.
Nếu bạn muốn task
saveInfo
chạy tuần tự thì hãy bỏ điTask.detached { ... }
nhóe. Lúc này, các tác vụ con sẽ thực hiện lần lượt và tất cả hoàn thành thì task vụ cha sẽ kết thúc.
Detached Task trong Actor
Với ví dụ trên, bạn cũng khó nhận ra được ý nghĩa của Detached Task trong chương trình. Vì hầu như mọi thứ đều chạy rất là nhanh. Nếu bạn áp dụng vào bài toán cụ thể tốn thời gian thực thi, thì sẽ được rõ ràng cách các task hoạt động như thế nào. Ahihi! Và Detached Task thì rất có nhiều lợi ích, tuy nhiên việc gì cũng có 2 mặt của nó.
Trong trường hợp này thì Detached Task sẽ ảnh hưởng trực tiếp tới Actor.
Nguyên nhân, các Detached Task sẽ được khởi chạy ở một ngữ cảnh khác. Nó không bị quản chế bởi các isolate từ các Actor. Khi bạn tạo một Task bình thường từ bên trong một Actor, nó sẽ bị tách biệt (isolate) với Actor đó, có nghĩa là bạn có thể sử dụng đồng bộ các phần khác của Actor đó.
Ví dụ nhóe!
actor User { func login() { Task { if authenticate(user: "taytay89", password: "n3wy0rk") { print("Successfully logged in.") } else { print("Sorry, something went wrong.") } } } func authenticate(user: String, password: String) -> Bool { // Complicated logic here return true } } // running Task { let user = User() await user.login() }
Trong khi đó, một Detached Task chạy đồng thời với tất cả các code khác, bao gồm cả Actor đã tạo ra nó (nó thực sự là mô côi cha mẹ), và do đó đã hạn chế rất nhiều quyền truy cập vào dữ liệu bên trong Actor. Xem tiếp ví dụ nhóe!
actor User { func login() { Task.detached { if await self.authenticate(user: "taytay89", password: "n3wy0rk") { print("Successfully logged in.") } else { print("Sorry, something went wrong.") } } } func authenticate(user: String, password: String) -> Bool { // Complicated logic here return true } }
Bạn sẽ dễ dàng nhận ra rằng là phải dùng tới await
để gọi authenticate
trong chính Actor của mình. Lúc này, Detached Task đã thoát khỏi actor. Nhất là khi bạn đang thực thi ở Main, nó sẽ chạy riêng ra một Thread khác.
Đây là điểm mà bạn cần phải rất chú ý, vì rất dễ phản tác dụng khi muốn tránh Data Race với Actor.
Ngữ cảnh hoạt động của Detached Tasks
Ta đã biết được sự ảnh hưởng của Detached Tasks đối với các Actor rồi. Và vẫn còn một điểm chú ý nữa khi bạn sử dụng Detached Task. Đó là ngữ cảnh mà nó sẽ được thực thi.
Hay còn gọi là Thread thôi, ahihi!
Ta sẽ sử dụng ví dụ để mình họa nhóe. Lần này, bạn nên tạo một iOS Project để có được Main Thread. Đầu tiên, ta cần tạo một extension cho DispatchQueue để biết được là đang ở Main Thread hay là không.
extension OperationQueue { static func mainQueueChecker() -> String { return Self.current == Self.main ? "✅" : "❌" } }
Bạn có 1 function đơn giản như sau:
func doWork() { Task { for i in 1...10 { print("🔵 #\(i) - MainThread is \(OperationQueue.mainQueueChecker())") print("In Task 1: \(i)") } } Task { for i in 1...10 { print("🔴 #\(i) - MainThread is \(OperationQueue.mainQueueChecker())") print("In Task 2: \(i)") } } }
Bạn thực thi chúng, thì thấy mọi việc hoạt động một cách tuần tự. Điều quan trọng là bạn không đủ số lượng và thời gian để thấy nó chạy từng task một. Nhưng điểm chú ý đó là tất cả vẫn chạy ở Main Thread.
🔵 #1 - MainThread is ✅ In Task 1: 1 🔵 #2 - MainThread is ✅ In Task 1: 2 🔵 #3 - MainThread is ✅ In Task 1: 3 🔵 #4 - MainThread is ✅ In Task 1: 4 🔵 #5 - MainThread is ✅ In Task 1: 5 🔵 #6 - MainThread is ✅ In Task 1: 6 🔵 #7 - MainThread is ✅ In Task 1: 7 🔵 #8 - MainThread is ✅ In Task 1: 8 🔵 #9 - MainThread is ✅ In Task 1: 9 🔵 #10 - MainThread is ✅ In Task 1: 10 🔴 #1 - MainThread is ✅ In Task 2: 1 🔴 #2 - MainThread is ✅ In Task 2: 2 🔴 #3 - MainThread is ✅ In Task 2: 3 🔴 #4 - MainThread is ✅ In Task 2: 4 🔴 #5 - MainThread is ✅ In Task 2: 5 🔴 #6 - MainThread is ✅ In Task 2: 6 🔴 #7 - MainThread is ✅ In Task 2: 7 🔴 #8 - MainThread is ✅ In Task 2: 8 🔴 #9 - MainThread is ✅ In Task 2: 9 🔴 #10 - MainThread is ✅ In Task 2: 10
Tiếp theo, bạn hãy thay đổi một tí, chuyển tử Task { }
thành Task.detached { }
nhóe.
func doWork() { Task.detached { for i in 1...10 { print("🔵 #\(i) - MainThread is \(OperationQueue.mainQueueChecker())") print("In Task 1: \(i)") } } Task.detached { for i in 1...10 { print("🔴 #\(i) - MainThread is \(OperationQueue.mainQueueChecker())") print("In Task 2: \(i)") } } }
Kết quả lần này đã khác rồi. Các task sẽ chạy ở Thread khác và không phải ở Main Thread. Nếu số lượng & thời gian đủ nhiều, thì bạn sẽ thấy các task xanh đỏ sẽ xen kẻ với nhau.
🔵 #1 - MainThread is ❌ In Task 1: 1 🔵 #2 - MainThread is ❌ In Task 1: 2 🔵 #3 - MainThread is ❌ In Task 1: 3 🔵 #4 - MainThread is ❌ In Task 1: 4 🔵 #5 - MainThread is ❌ In Task 1: 5 🔵 #6 - MainThread is ❌ In Task 1: 6 🔵 #7 - MainThread is ❌ In Task 1: 7 🔵 #8 - MainThread is ❌ In Task 1: 8 🔵 #9 - MainThread is ❌ In Task 1: 9 🔵 #10 - MainThread is ❌ In Task 1: 10 🔴 #1 - MainThread is ❌ In Task 2: 1 🔴 #2 - MainThread is ❌ In Task 2: 2 🔴 #3 - MainThread is ❌ In Task 2: 3 🔴 #4 - MainThread is ❌ In Task 2: 4 🔴 #5 - MainThread is ❌ In Task 2: 5 🔴 #6 - MainThread is ❌ In Task 2: 6 🔴 #7 - MainThread is ❌ In Task 2: 7 🔴 #8 - MainThread is ❌ In Task 2: 8 🔴 #9 - MainThread is ❌ In Task 2: 9 🔴 #10 - MainThread is ❌ In Task 2: 10
Với ví dụ đơn giản như vậy, thì bạn sẽ cảm nhận được ngữ cảnh mà các Detached Tasks hoạt động rồi đó. Lúc này, không phải thích thì dùng nhóe.
Xung quanh bạn sẽ toàn là bugs đó!
Sự tác động lên UI
Hệ quả trực tiếp từ ngữ cảnh hoạt động của Detached Tasks, là khi bạn dùng trên UI của bạn. Xem qua ví dụ này với IBAction nha.
@IBAction func tapme(_ sender: Any) { Task { nameLabel.text = "AAAA" } }
Trong đó,
- Tại sự kiện người dùng là
tapme
, bạn tạo ra một task bất đồng bộ để cập nhật nội dung củanameLabel
- Kết quả là ngay lập tức
nameLabel
sẽ được thay đổi nội dung
Vì Task đó vẫn là một task con tạo ra từ Main Thread và nó cập nhật được các UI trong chương trình. Bạn thử thay đổi Task thành Task.detached nhóe.
@IBAction func tapme(_ sender: Any) { Task.detached { await self.nameLabel.text = "AAAA" } }
Property ‘text’ isolated to global actor ‘MainActor’ can not be mutated from a non-isolated context
Đây là lỗi mà bạn nhận được sau khi fix hết các lỗi mà Xcode bắt được. Và nội dung lỗi cũng được đề cập rất cụ thể rồi. Đó chính là bạn không thể thay đổi được UI nếu không phải MainActor.
Như vậy, với Detached Task thì bạn hầu như sẽ không làm gì được với UI hay Main Thread.
Detached Tasks với SwiftUI
Mục đích lớn nhất khi Swift 5.5 đưa ra các khái niệm New Concurrency, là việc áp dụng nó vào SwiftUI. Nhằm khắc phục đi các nhược điểm mà với cách cũ không thực thi được. Do đó, Detached Tasks cũng có một sự tác động không nhỏ tới SwiftUI.
Đầu tiên, bạn hãy xem qua ví dụ View cơ bản này trước.
struct ContentView: View { @State private var name = "Anonymous" var body: some View { VStack { Text("Hello, \(name)!") Button("Authenticate") { Task { name = "Taylor" } } } } }
Bạn sẽ thấy ta sử dụng Task { }
trong action
của Button. Nó tương tự với cách bạn dùng với IBAction ở trên. Và vẫn hoạt động okay nhóe. Tiếp theo, bạn thử đổi từ Task { }
thành Task.detached { }
nhóe.
Button("Authenticate") { Task.detached { name = "Taylor" } }
Nó vẫn hoạt động được. Vì @State đảm bảo có thể an toàn thay đổi giá trị của nó trên bất kỳ thread nào. Nhưng câu chuyện sẽ khác với việc bạn sử dụng một Observable Object và dùng các publisher để phát đi sự thay đổi. Bạn xem tiếp ví dụ với các @ObservedObject và @StateObject nhóe.
class ViewModel: ObservableObject { @Published var name = "Hello" } struct ContentView: View { @StateObject private var model = ViewModel() var body: some View { VStack { Text("Hello, \(model.name)!") Button("Authenticate") { Task { model.name = "Taylor" } } } } }
Nội dung của Text vẫn có thể thay đổi được từ một Task { }
cập nhật lại giá trị của một Observable Object. Có nghĩa rằng vẫn ở trên cùng MainActor và bạn vẫn có thể thay đổi UI. Nhưng cập chuyện lại không thể thực thi được khi bạn sử dụng Task.detached { }
cho object đó.
Button("Authenticate") { Task.detached { model.name = "Taylor" } }
Property ‘model’ isolated to global actor ‘MainActor’ can not be mutated from a non-isolated context
Lỗi này cũng tương tự như ví dụ bạn dùng ở phần trên với IBAction. Qua tiếp ví dụ này, bạn sẽ thấy được sự khác biệt cơ bản giữa Task & Detached Task rồi nhóe. Mặc dù với SwiftUI thì bạn vẫn được bảo kê từ một số Wrapper Properties (như là @State) an toàn khi hoạt động trên nhiều Thread khác nhau rồi.
Tạm kết
- Tìm hiểu về Detached Tasks & cách hoạt động của nó
- Các đặc tính cơ bản của Detached Tasks
- Sử dụng Detached Task trong Actor
- Ngữ cảnh thực thi chung và sự ảnh hưởng tới UI hay MainActor
- Tương tác giữa Detached Tasks với SwiftUI
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Detached Tasks 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
- 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)