Chào mừng bạn đến với thế giới của Swift và Concurrency! Trong bài viết này, chúng ta sẽ khám phá một khái niệm quan trọng trong Swift 5.5: Actor. Nếu bạn đang tìm hiểu về cách quản lý dữ liệu an toàn trong môi trường đa luồng, thì Actor chính là chìa khóa.
Tại sao Actor lại quan trọng?
Trước khi đi sâu vào định nghĩa “Actor Là Gì”, hãy cùng tìm hiểu lý do tại sao nó lại trở nên quan trọng. Khi làm việc với các ứng dụng đa luồng, việc truy cập và sửa đổi dữ liệu từ nhiều luồng đồng thời có thể dẫn đến các vấn đề như Data Race (tranh chấp dữ liệu) và Race Condition (điều kiện chạy đua). Điều này xảy ra khi nhiều luồng cố gắng truy cập và thay đổi cùng một dữ liệu cùng một lúc, dẫn đến kết quả không mong muốn và khó dự đoán.
Swift đã giới thiệu async/await để giúp đơn giản hóa việc viết mã bất đồng bộ. Tuy nhiên, async/await không tự động giải quyết vấn đề Data Race. Đó là nơi Actor xuất hiện.
Ví dụ về Data Race với async/await
Hãy xem xét ví dụ sau để hiểu rõ hơn về vấn đề này:
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)
}
})
}
var count = 1
async {
count = await cong1(count)
}
async {
count = await cong2(count)
}
print("#3: (count) - (Thread.current)")
Trong ví dụ này, hai hàm cong1
và cong2
được thực thi đồng thời, cùng cố gắng sửa đổi biến count
. Kết quả có thể không chính xác do Data Race.
Ví dụ về Data Race trong Swift sử dụng async/await, minh họa sự xung đột khi nhiều luồng cố gắng truy cập và sửa đổi biến count cùng một lúc.
Actor là gì? Định nghĩa và đặc điểm
Vậy, “actor là gì” và nó giải quyết vấn đề này như thế nào?
Actor là một kiểu dữ liệu tham chiếu (reference type) trong Swift, được thiết kế để bảo vệ việc truy cập vào trạng thái có thể thay đổi của nó. Nói cách khác, Actor đảm bảo rằng chỉ có một luồng duy nhất có thể truy cập và sửa đổi dữ liệu bên trong Actor tại một thời điểm bất kỳ. Điều này giúp loại bỏ Data Race và Race Condition.
Đặc điểm chính của Actor:
- Kiểu dữ liệu tham chiếu: Tương tự như class, Actor là một kiểu dữ liệu tham chiếu.
- An toàn dữ liệu: Các thuộc tính của Actor được bảo vệ và chỉ có thể truy cập an toàn từ bên trong Actor hoặc thông qua các phương thức bất đồng bộ.
- Độc quyền truy cập: Chỉ một luồng có thể truy cập Actor tại một thời điểm, ngăn chặn Data Race.
Cú pháp của Actor
Khai báo một Actor trong Swift rất đơn giản, sử dụng từ khóa actor
:
actor MyNumber {
var value: Int
init(value: Int) {
self.value = value
}
func show() {
print(value)
}
}
Giải quyết Data Race với Actor
Bây giờ, hãy xem cách Actor có thể giải quyết vấn đề Data Race trong ví dụ trước:
actor MyNumber {
var value: Int
init(value: Int) {
self.value = value
}
func cong1() {
value += 1
}
func cong2() {
value += 2
}
}
let number = MyNumber(value: 0)
async {
await number.cong1()
}
async {
await number.cong2()
}
async {
print(await number.value) // Kết quả: 3
}
Trong ví dụ này, MyNumber
là một Actor. Các phương thức cong1
và cong2
được gọi thông qua await
, đảm bảo rằng chúng được thực thi một cách an toàn và không gây ra Data Race.
Actor Protocol
Actor cũng có thể tuân thủ các Protocol, tương tự như class và struct. Điều này cho phép bạn định nghĩa các giao diện chung cho các Actor khác nhau.
protocol P {
func show(value: Int) async
}
actor Counter {
var value = 0
func increment() {
value += 1
}
}
extension Counter: P {
func show(value: Int) async {
print(value)
}
}
Isolated và Nonisolated
Khi làm việc với Actor, bạn sẽ gặp khái niệm isolated
. Theo mặc định, tất cả các thuộc tính và phương thức của Actor đều là isolated
, nghĩa là chúng chỉ có thể được truy cập từ bên trong Actor hoặc thông qua các phương thức bất đồng bộ.
Nếu bạn muốn một phương thức có thể được truy cập đồng bộ từ bên ngoài Actor, bạn có thể sử dụng từ khóa nonisolated
. Tuy nhiên, bạn không thể sử dụng nonisolated
cho các thuộc tính của Actor.
actor Counter{
var value = 0
func increment() {
value = value + 1
}
}
// Conform
extension Counter: P {
nonisolated func show(value: Int) {
print(value)
}
}
Tương tác với Actor
Bên trong Actor
Bên trong Actor, bạn có thể truy cập các thuộc tính và phương thức của nó một cách bình thường.
actor MyNumber {
var value: Int
init(value: Int) {
self.value = value
}
func show() {
cong1()
value = 10
print(value)
}
func cong1() {
value += 1
}
}
Bên ngoài Actor
Để truy cập các thuộc tính và phương thức của Actor từ bên ngoài, bạn phải sử dụng await
trong một ngữ cảnh bất đồng bộ.
let temp = MyNumber(value: 1)
async {
await temp.show()
}
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:
async {
// await temp.value = 10 // Lỗi
// temp.value = 10 // Lỗi
await temp.show()
}
Tương tác giữa các Actor
Khi một Actor tương tác với một Actor khác, bạn cần sử dụng await
để đảm bảo an toàn dữ liệu.
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
}
}
let temp1 = MyNumber(value: 999)
let temp2 = MyNumber(value: 1)
async {
await temp1.sendValue(with: temp2)
print(await temp1.value)
await temp2.show()
}
Class vs. Actor: Sự khác biệt
Mặc dù class và actor có nhiều điểm tương đồng, nhưng có một số khác biệt quan trọng:
- Kế thừa: Actor không hỗ trợ kế thừa.
- Tính an toàn: Actor được thiết kế để đảm bảo an toàn dữ liệu trong môi trường đa luồng, trong khi class không có tính năng này theo mặc định.
- Isolated: Các thuộc tính và phương thức của Actor mặc định là
isolated
, trong khi class không có khái niệm này.
Tính năng | Class | Actor |
---|---|---|
Kiểu | Tham chiếu | Tham chiếu |
Kế thừa | Hỗ trợ | Không hỗ trợ |
An toàn dữ liệu | Không (cần các biện pháp bảo vệ thủ công) | Có (tự động bảo vệ truy cập đồng thời) |
Kết luận
Trong bài viết này, chúng ta đã tìm hiểu về “actor là gì”, cách nó giải quyết vấn đề Data Race và Race Condition trong Swift, và cách sử dụng nó để viết mã an toàn hơn trong môi trường đa luồng. Actor là một công cụ mạnh mẽ giúp bạn quản lý dữ liệu an toàn và hiệu quả trong các ứng dụng Swift hiện đại. Hãy thử nghiệm và khám phá thêm về Actor để nâng cao kỹ năng lập trình của bạn!