Sendable Protocol & @Sendable trong 10 phút – Swift 5.5
iOS & SwiftContents
Chào mừng bạn đến với Fx Studio. Chúng ta lại tiếp tục cuộc hành trình trong thế giới Concurrency mới của Swift. Chủ đề lần này là về một kiểu dữ liệu mới, được xuất hiện trong Swift 5.5. Đó là Sendable. Đi kèm với đó là Sendable Protocol & @Sendable để giúp bạn an toàn hơn khi sử dụng trong các quá trình xử lý đồng thời (concurrent).
Đây cũng là một khái niệm nâng cao trong Swift & Concurrency mới của nó. Nó có liên quan mật thiết tới các Actor. Và bạn có thể tham khảo các bài viết mới sau đây để có thể hiểu hơn về chúng.
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.
The Sendable Type
Khi bạn đã bước vào vũ trụ Concurrency rồi, thì việc đảm bảo an toàn cho dữ liệu của bạn nhiều luồng khác nhau là rất quan trọng. Nhưng bên cạnh đó việc truyền dữ liệu đi cũng khá là đau đầu. Và Swift 5.5, cung cấp thêm khái niệm cho bạn một kiểu dữ liệu mới. Đó là Sendable Type.
Sendable Type là kiểu dữ liệu mà có thể được chia sẽ một cách an toàn trong Concurrency.
Với các Class truyền thống, muốn gởi đi được (như Sendable) thì nó phải là bất biến (immutable) hoặc có khả năng đồng bộ hóa bên trong chúng (như actor). Hoặc bạn muốn sử dụng tương tự như các kiểu Class (tham chiếu), thì có thể sử dụng Actor để thay thế. Hoặc bạn phải tương tác giữa các Actor và các kiểu dữ liệu khác trong project của bạn … các kiểu con đà điểu. Sẽ ép bạn tới công chuyện là dùng tới Sendable Type.
Việc thêm mới phần này trong Swift là nhằm hỗ trợ thêm cho dữ liệu an toàn khi gởi đi, là những dữ liệu được chuyển sang một thread khác một cách an toàn. Quan trọng nhất là cách mà bạn có thể hạn chế đi rất nhiều
Data Races
Điều này có thể thực hiện thông qua 2 cách (hay là 2 ứng dụng của nó):
-
- Sendable Protocol
- @Sendable
Sendable Type với các kiểu dữ liệu
Ta sẽ khám phá dần dần việc sử dụng Sendable Type như thế nào đối với các kiểu dữ liệu mà chúng ta đang có.
Struct & Class
Đầu tiên, chính là các kiểu dữ liệu có sẵn. Như Int, Float, … thì hệ thống cũng đã hỗ trợ sẵn rồi. Do đó, trình biên dịch của Swift cũng tự động hiểu giúp bạn. Hoặc dành cho bạn nào rãnh rỗi thì có thể viết lại cũng được.
extension Int: Sendable {}
Conformance of ‘Int’ to protocol ‘Sendable’ was already stated in the type’s module ‘Swift’
Sẽ nhận được 1 cảnh báo từ trình biên dịch, là em này đã được tạo rồi. Còn với các Struct của riêng bạn, thì cũng được bao kê luôn với Sendable Type luôn.
struct Article { var views: Int }
Bởi vì các struct là kiểu value type.
Còn với kiểu tham chiếu như là Class thì mặc định là sẽ không tương thích với Sendable Type.
class User { var name: String = "" }
Vì Class là kiểu tham chiếu và đối tượng của nó thì có thể thay đổi từ các concurrency khác. Nói cách khác, class User (trong ví dụ) không an toàn để truyền đi và trình biên dịch không thể đánh dấu hoàn toàn nó là một Sendable được.
Generic & Enum
Với việc khai báo các struct/class là một Generic, thì trình biên dịch sẽ hiểu là không tích hợp với Sendable Type. Để bạn có thể sử dụng với chúngm thì bản thân Generic đó cũng phải conform Sendable Protocol luôn nhóe.
struct Container<Value: Sendable> { var child: Value }
Cũng tương tự như với các kiểu Enum. Và một điều cần nhớ là các kiểu dữ liệu cho bản thân các properties của chúng của phải tương thích với Sendable.
enum State: Sendable { case loggedOut case loggedIn(name: NSMutableString) // Error }
Chỉnh sửa lại kiểu String thay cho NSMutableString thì Enum State của chúng hoàn toàn đảm bảo với Sendable Type.
enum State: Sendable { case loggedOut case loggedIn(name: String) }
Throwing Errors
Cũng áp dụng nguyên tắc của Sendable Type cho các Error của chúng ta.
struct ArticleSavingError: Error { var author: NonFinalAuthor } extension ArticleSavingError: Sendable { }
Trong đó, bạn cần đảm bảo thuộc tính author
phải tương thích với Sendable Type, thì cả struct Error đó sẽ tương tích với Sendable Type.
Sendable Protocol
Chắc bạn cũng phần nào đoán ra được cách tạo ra một kiểu Sendable rồi đó. Nếu như Class của chúng ta chưa thỏa mãn yêu cầu và bạn chưa biết gì về Actor. Thì đây xem như là cách đơn giản nhất rồi nhóe.
Vì nó là Protocol nên ta có thể tích hợp vào nhiều kiểu dữ liệu có sẵn, như: class, struct, enum … Tuy nhiên, chúng ta có nhiều thứ an toàn khi chuyển qua lại giữa các Thread:
- Những kiểu dữ liệu cơ bản. Như Int, Bool, String
- Các Optionals nơi dữ liệu được bọc lại là một kiểu giá trị
- Các loại Collections với kiểu dữ liệu cho phần tử là kiểu giá trị
- Typle với kiểu dữ liệu các phần tử vẫn là value
- Metatypes, như String.self
Bạn sẽ thấy chúng nó đều bà con với
value type
phải không nào.
Một số kiểu custom cần phải cập nhật như sau để phù hợp với Sendable Protocol:
- Với các Actor tự động conform với Sendable Protocol, vì chúng buộc phải đồng bộ dữ liệu trong môi trường hoạt động đồng thời
- Các Struct & enum cũng tương thích với Sendable, nếu bản thân nó có các thuộc tính tương thích với Sendable (tương tự như Codable)
- Còn các Class có thể thương thích với Sendable Protocol, miễn là chúng kế thừa lại NSObject hoăc không gì cả. Các thuộc tính của nó cũng phải tương thích với Sendable. Cuối cùng là
final
để không có sub-class của nó.
Sử dụng Sendable Protocol
Ở đây, nhiều kiểu dữ liệu đã ngầm định tương thích rồi thì chúng ta sẽ không cần conform thêm với Sendable Protocol nữa. Cũng giảm tải đi một phần công việc. Tuy nhiên, với vài đại diện như Class hoặc vài trường hợp sẽ không được tương thích ngầm định. Do đó, bạn cũng phải biết cách để đảm bảo dữ liệu của chúng ta an toàn trong Concurrency với Sendable Protocol
Điển hình cho các trường hợp này là Class. Và đây là cách bạn sử dụng Class với Sendable Protocol nhé.
final class User: Sendable { let name: String init(name: String) { self.name = name } }
Trong đó, bạn chú ý 2 điểm với một immutable class tương thích với Sendable Protocol là final
& Sendable
.
Với khai náo này thì User là một immutable class. Nó sẽ được an toàn khi chuyển đi giữa các thread trong Concurrency. Đồng nghĩa với đó là User tương thích với Sendable.
Trái ngược với immutable class là mutable class. Mutable class cần được đánh dấu bằng thuộc tính @unchecked
để cho biết lớp của chúng ta thực sự an toàn theo luồng do cơ chế khóa bên trong. Ví dụ như sau:
extension DispatchQueue { static let userMutatingLock = DispatchQueue(label: "person.lock.queue") } final class MutableUser: @unchecked Sendable { private var name: String = "" func updateName(_ name: String) { DispatchQueue.userMutatingLock.sync { self.name = name } } }
Như vậy, bạn đã nắm được sơ lược cách sử dụng Sendable Protocol rồi nhóe. Mặc dù hơi khó hiểu, vì chúng ta khó có được một ví dụ cụ thể minh họa. Tạm chấp nhận vậy đi nha.
Nguyên tắc với Sendable Protocol
Đầu tiên, việc tương thích với Sendable Protocol xãy ra khi tất cả thuộc tính trong class/struct/enum đó đều phải tương thích với Sendable Protocol.
public struct Article { internal var title: String }
Trong ví dụ trên, với Article thì bản thân nó là public
, nhưng title
của nó lại là internal
. Do đó, trình biên dịch sẽ xác nhận nó không tương thích với Sendable và không thể nhìn thấy được title
. Mặc dù, title
là kiểu String, cũng làm một kiểu tương thích ngầm định với Sendable.
Điểm thứ hai là phải sử dụng final
đối với các Class muốn tương thích hoàn toàn với Sendable Protocol. Cái này mình đã có ví dụ ở trên.
Hoặc một cách hiểu khác trong trường hợp bạn muốn class/struct của bạn vẫn tương thích với Sendable Protocol nhưng sẽ bỏ qua đi việc kiểm tra từ trình biến dịch thì hãy sử dụng nới với @unchecked.
// MySneakyNSPerson.swift struct MySneakyNSPerson { private var name: NSMutableString public var age: Int } // in another source file or module... // error: cannot declare conformance to Sendable outside of // the source file defined MySneakyNSPerson extension MySneakyNSPerson: Sendable { } // in another source file or module... // okay: unchecked conformances in a different source file are permitted extension MySneakyNSPerson: @unchecked Sendable { }
Với @unchecked, bạn cẩn phải đảm rằng các kiểu của bạn hoàn toàn an toàn trong các thread. Số phận chương trình phụ thuộc vào bạn.
@Sendable closure
Như bạn đã biết, các function vẫn có thể được gởi đi qua các thread khác nhau trong việc tương tác Concurrency. Nó cũng được xem như là một biến, điển hình như closure. Nó cũng mang trong mình giá trị. Nhưng nó lại không conform được với Sendable Protocol. Và cũng để đảm bảo các function & closure được an toàn trong các thread đồng thời, thì @Sendable được ra đời.
Ví dụ, áp dụng cho các hàm global, các closure, các setter & getter … Bằng cách sử dụng thuộc tính @Sendable, chúng ta sẽ cho trình biên dịch biết rằng không cần đồng bộ hóa bổ sung, vì tất cả các giá trị được ghi lại (capture) trong bao đóng đều an toàn theo luồng để làm việc.
Một ví dụ điển hình sẽ là sử dụng các bao đóng từ bên trong Actor isolation:
struct Article { var title: String } actor ArticlesList { private let articles = [ Article(title: "Swift Article 01"), Article(title: "Swift Article 02"), Article(title: "Swift Article 03"), ] func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] { // .... } }
Và khi bạn sử dụng closure với kiểu non-sendable thì sẽ bị báo lỗi. Hiểu đơn giản là closure của bạn có sử dụng các biến & giá trị mà chúng lại không tương thích với Sendable hoặc chúng có thể bị sửa đổi được từ các thread khác trong concurrency. Đó chính là một closure non-sendable.
let listOfArticles = ArticlesList() var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword") let filteredArticles = await listOfArticles.filteredArticles { article in // Error: Reference to captured var 'searchKeyword' in concurrently-executing code guard let searchKeyword = searchKeyword else { return false } return article.title == searchKeyword.string }
Trong đó, NSAttributedString sẽ không tương thích với Sendable Protocol. Bạn có thể fix bug này với việc thay đổi sang String. Nhưng nó chứng minh cách trình biên dịch giúp chúng ta thực thi luồng an toàn (safe-thread) trong concurrency.
Tạm kết
Về bài viết, mình mới chỉ cung cấp cho bạn các khái niệm cơ bản về kiểu dữ liệu mới này. Và cách bạn sử dụng nó cho việc khai báo các kiểu dữ liệu của bạn & trong các function …. Hẹn bạn ở một bài viết khác nói về việc áp dụng Sendable Type trong vấn đề giải quyết Datas Race trong Swift.
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Sendable Type 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.
- Bài viết tiếp theo tại đây.
- Tham khảo: Sendable and @Sendable closures
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)