Contents
Chào mừng bạn đến với Fx Studio. Chúng ta lại tiếp tục tìm hiểu về những thứ mới trong Swift 5.5 vừa ra mắt. Đối tượng lần này là Actor. Một thực thể mới được Apple giới thiệu trong Swift. Nó sẽ giúp bạn rất nhiều trong làm việc với Concurrency mới.
Nếu bạn chưa biết gì về async/await, bạn hãy truy cập link dưới đây để bổ tục kiến thức nha.
Còn nếu mọi thứ đã ổn rồi, thì …
Bắt đầu thôi!
Chuẩn bị
Sự kiện lớn nhất của Apple vào đầu tháng 6/2021 vừa qua là WWDC 21. Apple đã giới thiệu cho cộng đồng dev iOS về Swift 5.5, trong đó có sự cập nhật rất là lớn. Trong số đó, Apple đã giới thiệu về Actor trong Swift. Do đó, bạn cần phải chuẩn bị như sau:
-
- Swift 5.5
- Xcode 13 (beta)
Thật là khó khi Xcode 13 khá là nặng, nên có thể Macbook của bạn sẽ chạy không nỗi. Nên bạn cũng phải cân nhắc khi cập nhật macOS và Xcode 13 nhoé.
Cộng với async/await thì Actor sẽ hỗ trợ chúng một cách rất mạnh mẽ. Nên về mặt kiến thức, bạn cần phải hiểu được async/await trước. Ngoài ra, Actor ra đời cũng giải quyết vấn đề Data Race và bạn cũng cần cập nhật thêm kiến thức về Data Race là gì. (bạn có thể tham khảo ở link dưới đây)
Về mặt demo, ta sẽ demo với Playgroup hoặc với một Project iOS nào đó cũng được. Do sẽ làm việc với Console là chính, nên giao diện lúc này là không cần thiết.
1. Actor là gì?
1.1. Nguyên nhân ra đời
Ở bài viết trước về Data Race & cách giải quyết nó. Thì bạn đã biết là vì nguyên nhân xử lý Concurrency chưa được tốt, dẫn tới sự sai sót về mặt dữ liệu cho các biến dùng chung (share resources).
Bạn cũng sẽ nghĩ rằng: “Swift ra mắt async/await rồi còn gì. EZ nhoé!”
Nhưng … rồi ai cũng phải gặp lỗi thôi. Mặc dù async/await giúp bạn cho 1 đoạn code hay function hoạt động một cách đồng thời với nhau. Tuy nhiên, cũng chính về lợi điểm đó, mà dẫn tới sự chạy đua vũ trang về mặt dữ liệu (Data Race) sẽ rất khốc liệt.
Ta hãy xem một ví như sau nha. Khai báo 2 function đơn giản với async
, trong đó:
- Thực thi việc tính toán ở một thread khác
- Trả kết quả về sau khi đã tính toán thành công
- Một hàm tăng
1
và một hàm tăng2
func cong1(_ count: Int) async -> Int { await withCheckedContinuation({ c in DispatchQueue.global().async { print("#1: \(count) - \(Thread.current)") c.resume(returning: count + 1) } }) } func cong2(_ count: Int) async -> Int { await withCheckedContinuation({ c in DispatchQueue.global().async { print("#2: \(count) - \(Thread.current)") c.resume(returning: count + 2) } }) }
Chúng ta sẽ dùng nó như thế này.
var count = 1 async { count = await cong1(count) } async { count = await cong2(count) } print("#3: \(count) - \(Thread.current)")
Tuy nhiên, kết quả lại như thế này.
#3: 1 - <NSThread: 0x600002ed4140>{number = 1, name = main} #2: 1 - <NSThread: 0x600002eebc00>{number = 6, name = (null)} #1: 1 - <NSThread: 0x600002eebdc0>{number = 7, name = (null)}
Đó là do Data Race gây ra. Khi bạn dùng không chuẩn async/await. Còn nếu bạn dùng nó tuần tự như sau, thì sẽ không có hiện tượng như vậy. Tham khảo code nhoé!
async { count = await cong1(count) count = await cong2(count) print("#3: \(count) - \(Thread.current)") }
Kết quả như sau:
#1: 1 - <NSThread: 0x60000211b100>{number = 6, name = (null)} #2: 2 - <NSThread: 0x60000211b100>{number = 6, name = (null)} #3: 4 - <NSThread: 0x60000215c3c0>{number = 1, name = main}
Vì vậy, bạn sẽ cần một giải pháp (mới) để đảm bảo an toàn về mặt dữ liệu trong quá trình tương tác đồng thời của async/await. Tránh các hậu quả không đáng có, mà do Data Race & Race Condition gây ra.
1.2. Khái niệm
Khi đã có rất nhiều giải pháp cho vấn đề Data Race rồi, thì Actor ra đời không góp phần thêm bạn thêm vui nữa. Mà lại mang một tính chất khác trong Concurrency Roadmap mới của Swift. Nó hỗ trợ cho async/await thêm mạnh mẽ hơn nữa.
Xin phép dịch cho Actor là Tác nhân. Vì nhiều bạn cứ nói rằng những khái niệm lập trình mà dịch ra chuối lắm. Do đó, mình thêm 1 trái chuối nữa cho đủ 1 nải chuối. Ahihi!
Ta có định nghĩa Actor như sau:
Actor là một dữ liệu tham chiếu (reference type) mà bảo vệ việc truy cập vào các trạng thái có thể thay đổi được của nó.
Về mặt khái niệm ban đầu, thì tương tự như một class an toàn trong môi trường Concurrency. Vì tư tưởng Swift đảm bảo rằng:
Trạng thái của tác nhân (Actor) chỉ được truy cập bởi một luồng duy nhất tại bất cứ thời điểm nào. Giúp loại bỏ đi nhiều lỗi nghiêm trọng ngay ở level compiler.
Tóm tắt lại các đặc điểm của một Actor đó là:
- Là một kiểu dữ liệu tham chiếu, tương tự như class
- Các thuộc tính của nó sẽ được đảm bảo an toàn
- Chỉ cho phép mỗi thời điểm chỉ một thread có thể truy cập được.
Như vậy, bạn cũng đủ hình dung ra Actor là như thế nào rồi. Bây giờ, chúng ta sẽ sang phần khai báo và sử dụng nó thôi.
1.3. Cú pháp
Một Actor sẽ được khai báo bằng một từ khoá là actor
. Ta sẽ thử khai báo thông qua ví dụ sau:
actor MyNumber { var value: Int init(value: Int) { self.value = value } func show() { print(value) } }
Khá đơn giản phải không nào. Bạn chỉ cần sử dụng từ khoá actor
như class
. Mọi việc còn lại vẫn như bạn thực hiện khai báo một class vậy.
- Bạn vẫn có thể khai báo các thuộc tính cho Actor
- Khai báo thêm các hàm khởi tạo
init
nếu cần thiết - Khai báo các function làm phương thức cho Actor
Bây giờ, ngoài các thế lực kiểu dữ liệu kinh điển như là Class & Struct. Bạn sẽ có thêm một thế lực nữa là Actor.
Ta sẽ giải quyết bài toán Data Race ở trên cự kì đơn giản với Actor vừa khai báo. Bạn tham khảo ví dụ code cho việc sử dụng một Actor nhoé.
var number = MyNumber(value: 0) async { await number.cong1() } async { await number.cong2() } async { print(await number.value) }
Build và cảm nhận kết quả nhận được đúng là 0 + 1 + 2 = 3
rồi nhoé. Không có việc bon chen hay chạy đua về mặt dữ liệu ở đây. Do đó, giá trị dữ liệu đúng như ý đồ của chúng ta.
1.4. Actor Protocol
Swift còn cho Actor thêm sức mạnh khi có thể implement thêm sức mạnh của Protocol. Hoặc bạn có thể khai báo các Actor Protocol và tiến hành conform vào các actor khác.
Phần này quá ít sách vở ghi quá, nên mình không tìm được thêm các thông tin khác hay các lưu ý về Actor Protocol cho bạn.
Bạn xem qua ví dụ sau:
// Protocol protocol P { func show(value: Int) async } // Actor actor Counter{ var value = 0 func increment() { value = value + 1 } } // Conform extension Counter: P { func show(value: Int) async { print(value) } }
Trong đó:
P
là một protocol như bình thường. Tuy nhiên, chúng ta có thêm một function vớiasync
Counter
là một Actor cũng như bình thường- Cuối cùng, chúng ta cho Counter conform với P. Để cho Counter có thêm một function đến từ
P
1.5. isolated
isolated cũng là một khái niệm đi kèm với Actor. Nó có ý nghĩa là cách ly. Hiểu nôm na:
- Đây là giải pháp mà Actor đưa ra để đảm bảo việc một thread có thể thay đổi trạng thái của nó tại một thời điểm.
- Các Store Properties và Methods sẽ bị cách ly
- Chúng không thể truy cập được từ bên ngoài Actor, trừ khi chúng được thực hiện trong bất đồng bộ.
- Các Store Properties cũng không thể bị ghi đè từ bên ngoài Actor.
Mặc định với khai báo một
actor
thì tất cả Store Properties và Methods sẽ là isolated.
Nếu bạn không muốn isolated thì có thể dùng từ khoá nonisolated
trước khai báo function. Tuy nhiên, bạn không thể dùng nonisolated
cho các Store Properties của chính Actor.
Đúng là không lối thoát. Nhưng chúng có thể cho ta một trải nghiệm khi Actor conform các Protocol như các class/struct/enum bình thường khác.
Xem ví dụ nha.
// Protocol protocol P { func show(value: Int) } // Actor actor Counter{ var value = 0 func increment() { value = value + 1 } } // Conform extension Counter: P { nonisolated func show(value: Int) { print(value) } }
Ví dụ, là chính ví dụ trên. Nhưng biến tấu lại một chút thôi. Không có gì khó hết.
- Bạn sẽ bỏ đi
async
của function trong Protocol P - Nhưng lại phải thêm
nonisolated
cho việcextension
của Counter với P
Bây giờ, bạn đã biết được về isolated & nonisolated rồi. Nhưng theo lời khuyên, thì bạn hãy để mặc định hết đi. Và đừng cho nonisolated xuất hiện trong Actor. Vì phần sau này sẽ hại não hơn đó. Ahihi!
2. Tương tác
Lý thuyết đoạ rồi, chúng ta chuyển qua xem cách sử dụng của nó như thế nào nha.
2.1. Bên trong
Ta sẽ lấy lại ví dụ Actor MyNumber ở trên. Lần này, ta có một chút thay đổi như sau:
actor MyNumber { var value: Int init(value: Int) { self.value = value } func show() { cong1() value = 10 print(value) } func cong1() { value += 1 } func cong2() { value += 2 } }
Bạn hãy để ý tới function show()
, trong đó:
- Bạn lại gọi một function khác (là
cong1()
) - Xét lại giá trị của
value
- Đọc giá trị của
value
để in ra console
Mọi thứ hoàn toàn bình thường. Tuy nhiên, tất cả chỉ là bên trong nội tại của Actor hay chính trong đối tượng Actor đó mà thôi. Mọi thứ sẽ bắt đầu khó hơn khi bạn sử dụng ở bên ngoài.
2.2. Bên ngoài
Bạn sẽ khai báo một đối tượng Actor ở trên và gọi function show()
của nó. Code ví dụ như sau:
let temp = MyNumber(value: 1) temp.show()
Nếu bạn thực thi đoạn code đó, thì sẽ gặp lỗi ngay.
Vì Actor sẽ và chỉ dùng được trong bất đồng bộ mà thôi. Các function của nó cũng là các function bất đồng bộ (async
). Tuy nhiên, với khai báo actor
thì các function của nó sẽ bị lượt bỏ đi từ khoá async
.
Do đó, bạn muốn triệu hồi được phương thức của một Actor, thì có 2 cách sau:
- Bạn chỉ có thể gọi nó trong một function
async
khác - Trong khối lệnh
async { }
.
Xem lại ví dụ trên nha.
let temp = MyNumber(value: 1) async { await temp.show() }
Trong đó:
- Bạn cần thêm từ khoá
await
để gọi thực thi một function của Actor - Ngoài ra, bạn không thể truy cập trực tiếp các thuộc tính của Actor từ bên ngoài.
Bạn tham khảo tiếp ví dụ nha.
async { await temp.value = 10 //error temp.value = 10 //error await temp.show() }
Thực thi nó để cảm nhận thêm lỗi nhoé. Ahihi!
2.3. Actor khác
Ta hãy thử tiếp việc tương tác với một Actor khác bên trong một Actor thì như thế nào? Nó có dẫn tới các sự thay đổi nào thêm không? … Hãy bắt đầu bằng thêm function cho Actor MyNumber của chúng ta ở trên để làm ví dụ.
actor MyNumber { var value: Int init(value: Int) { self.value = value } func show() { print(value) } func sendValue(with otherNumber: MyNumber) async { let value = self.value await otherNumber.setValue(value) setValue(0) } func setValue(_ temp: Int) { value = temp } func cong1() { value += 1 } func cong2() { value += 2 } }
Trong đó:
- Thêm 2 function là
sendValue
&setValue
setValue
là một function bình thường của Actor. Nó tương tác với property của chính nó.sendValue
có chút khác biệt, vì ta sử dụng một đối tượng Actor khác để tương tác.
Bên trong sendValue
, ta sử dụng một Actor khác và nó lại gọi tới một function của chính nó. Trong trường hợp này:
- Bạn sẽ cần phải khai báo thêm từ khoá
async
cho functionsendValue
- Dùng
await
khi đối tượng Actor khác đó, để nó gọi thực thi chính function của nó.
Vì trong lúc otherNumber.setValue(value)
thực thi, thì có thể chính bản thân nó cũng thay đổi giá trị thuộc tính của chính nó ở một vị trí nào khác. Nên chúng ta cần await
để đối tượng Actor khác đó thực thi xong. Sau cùng, thì mới thực hiện các lệnh tiếp theo của Actor chính.
Ta xem tiếp ví dụ cho 2 Actor tương tác với nhau nha.
let temp1 = MyNumber(value: 999) let temp2 = MyNumber(value: 1) async { await temp1.sendValue(with: temp2) print(await temp1.value) await temp2.show() }
Build và cảm nhận kết quả nhoé. Ahihi!
3. Class vs. Actor
3.1. Giống
- Cả 2 đều là kiểu dữ liệu tham chiếu
- Đều có methods, properties, init, subscripts
- Có thể kế thừa các protocol và generic
- Các Phương thức static đều hoạt động giống nhau cho cả 2. Vì không có self, nên không có lý thuyết isolated
3.2. Khác
- Actor không hỗ trợ thừa kế, các hàm khởi tạo có nó sẽ đơn giản hơn.
- Không có Convenience Init, overriding, final
- Actor conform được với các Actor Protocol. Và Actor Protocol cũng phải đảm bảo rằng tất cả đều tuân thủ theo nguyên tắc isolated.
Tạm kết
- Thêm một cách đảm bảo dữ liệu được an toàn trong Concurrency
- Thêm một kiểu dữ liệu mới
- Khái niệm & cách sử dụng cơ bản của Actor
Okay! Tới đây, mình xin kết thúc bài viết giới thiệu về Actor trong Swift 5.5 . Như đã trình bày ở trên, nếu có gì thay đổi hay cập nhật thêm, mình sẽ tiếp tục update cho bài viết. Với Swift 5.5 còn rất nhiều những thứ hay nữa, nên bạn hãy tiếp tục theo dõi và chờ đón các bài viết sau nha.
Và 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.
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)