Актор — это ссылочный тип, подобный классу, созданный для безопасного управления изменяемым состоянием в многопоточной среде. Появившиеся в Swift 5.5, акторы помогают разработчикам писать потокобезопасный и более предсказуемый код при работе с асинхронными задачами и общими изменяемыми данными.
Чем акторы отличаются от классов?
Акторы гарантируют, что к их изменяемому состоянию одновременно имеет доступ только один поток. Это предотвращает повреждение данных и обеспечивает целостность.
Давайте рассмотрим простой пример с использованием класса Dinner. У него есть одна переменная и один метод eat(), который увеличивает счётчик приготовленных ужинов за месяц:
class Dinner {
var value = 0
func eat() {
value += 1
}
}
let dinner = Dinner()
// Simulate concurrent access
Task {
await withTaskGroup(of: Void.self) { group in
for i in 0..<30 {
if i % 2 == 0 { dinner.value -= 1 }
group.addTask {
dinner.eat() // ⚠️ Not thread-safe!
}
}
}
print("Final value (class): \(dinner.value)")
}
Здесь я намеренно добавил условие, чтобы повлиять на счётчик. Хотя это простой пример, в реальном приложении более сложные случаи могут вызвать похожие проблемы. Вместо 30 вы можете получить 15 или даже меньше. Это связано с тем, что к значению обращаются и изменяют его из нескольких потоков одновременно.
Даже если вы сделаете value приватным и будете изменять его только через функцию, вы всё равно не сможете предотвратить состояние гонки в асинхронном коде.
Как акторы решают эту проблему
С акторами эта проблема исчезает. Вы просто не можете получить доступ к внутреннему состоянию актора напрямую извне. Компилятор не позволит вам.
actor SafeMeal {
var value = 0
func eat() {
value += 1
}
func getValue() -> Int {
return value
}
}
let safeMeal = SafeMeal()
// Safe concurrent access
Task {
await withTaskGroup(of: Void.self) { group in
for _ in 0..<30 {
// if i % 2 == 0 { safeMeal.value = 1 } ⚠️ Not allowed, will cause a compile-time error
group.addTask {
await safeMeal.eat() // ✅ Safe access
}
}
}
let finalValue = await safeMeal.getValue()
print("Final value (actor): \(finalValue)")
}
Если вы попытаетесь изменить значение напрямую, вы получите ошибку следующего вида:
Actor-isolated property ‘value’ cannot be mutated from a non-isolated context.
Чтобы изменить значение, необходимо определить метод внутри актора, а затем вызвать его с помощью await:
func reduceCount() {
value -= 1
}
if i % 2 == 0 {
await safeMeal.reduceCount()
}
Это обеспечивает безопасный доступ к актору, по одной задаче за раз.
Продвинутый пример: чат-сервер
// Define a message model
struct Message {
let user: String
let content: String
let timestamp: Date
}
// Actor to manage chat history
actor ChatServer {
private var messages: [Message] = []
func postMessage(from user: String, content: String) {
let message = Message(user: user, content: content, timestamp: Date())
messages.append(message)
}
func getMessages() -> [Message] {
return messages.sorted { $0.timestamp < $1.timestamp }
}
}
struct Main {
static func chat() async {
let server = ChatServer()
let users = ["Alice", "Bob", "Charlie", "Diana"]
print("Starting to post messages...")
await withTaskGroup(of: Void.self) { group in
for user in users {
group.addTask {
for i in 1...5 {
print("Posting message \(i) from \(user)")
await server.postMessage(from: user, content: "Message \(i) from \(user)")
try? await Task.sleep(nanoseconds: UInt64.random(in: 10_000_000...50_000_000))
}
}
}
}
print("Finished posting messages. Retrieving all messages...")
let allMessages = await server.getMessages()
for message in allMessages {
print("[\(message.timestamp)] \(message.user): \(message.content)")
}
print("All messages printed.")
}
}
В этом примере актор обеспечивает безопасное обновление сообщений, даже если несколько пользователей публикуют сообщения одновременно.
Starting to post messages... Posting message 1 from Alice Posting message 1 from Bob Posting message 1 from Charlie Posting message 1 from Diana Posting message 2 from Alice Posting message 2 from Bob Posting message 2 from Diana Posting message 2 from Charlie Posting message 3 from Alice Posting message 3 from Bob Posting message 3 from Charlie Posting message 3 from Diana Posting message 4 from Charlie Posting message 4 from Diana Posting message 4 from Bob Posting message 4 from Alice Posting message 5 from Diana Posting message 5 from Charlie Posting message 5 from Bob Posting message 5 from Alice Finished posting messages. Retrieving all messages... [2025-04-17 19:14:11 +0000] Alice: Message 1 from Alice [2025-04-17 19:14:11 +0000] Bob: Message 1 from Bob [2025-04-17 19:14:11 +0000] Charlie: Message 1 from Charlie [2025-04-17 19:14:11 +0000] Diana: Message 1 from Diana [2025-04-17 19:14:11 +0000] Alice: Message 2 from Alice [2025-04-17 19:14:11 +0000] Bob: Message 2 from Bob [2025-04-17 19:14:11 +0000] Diana: Message 2 from Diana [2025-04-17 19:14:11 +0000] Charlie: Message 2 from Charlie [2025-04-17 19:14:11 +0000] Alice: Message 3 from Alice [2025-04-17 19:14:11 +0000] Bob: Message 3 from Bob [2025-04-17 19:14:11 +0000] Charlie: Message 3 from Charlie [2025-04-17 19:14:11 +0000] Diana: Message 3 from Diana [2025-04-17 19:14:11 +0000] Charlie: Message 4 from Charlie [2025-04-17 19:14:11 +0000] Diana: Message 4 from Diana [2025-04-17 19:14:11 +0000] Bob: Message 4 from Bob [2025-04-17 19:14:11 +0000] Alice: Message 4 from Alice [2025-04-17 19:14:11 +0000] Diana: Message 5 from Diana [2025-04-17 19:14:11 +0000] Charlie: Message 5 from Charlie [2025-04-17 19:14:11 +0000] Bob: Message 5 from Bob [2025-04-17 19:14:11 +0000] Alice: Message 5 from Alice All messages printed.
Что, если бы мы использовали класс?
А теперь представьте, что это делается с классом вместо актора. При 100 сообщениях на пользователя всё выглядит следующим образом:
// Message model remains the same
class Message {
let user: String
let content: String
let timestamp: Date
init(user: String, content: String, timestamp: Date) {
self.user = user
self.content = content
self.timestamp = timestamp
}
}
// Class to manage chat history (NOT thread-safe)
class ChatServer {
private var messages: [Message] = []
func postMessage(from user: String, content: String) {
let message = Message(user: user, content: content, timestamp: Date())
messages.append(message) // ⚠️ Potential data race
}
func getMessages() -> [Message] {
return messages.sorted { $0.timestamp < $1.timestamp }
}
}
// Main simulation
struct Main {
static func chat() async {
let server = ChatServer()
let users = ["Alice", "Bob", "Charlie", "Diana"]
print("Starting to post messages...")
await withTaskGroup(of: Void.self) { group in
for user in users {
group.addTask {
for i in 1...100 {
print("Posting message \(i) from \(user)")
server.postMessage(from: user, content: "Message \(i) from \(user)")
try? await Task.sleep(nanoseconds: UInt64.random(in: 10_000_000...50_000_000))
}
}
}
}
print("Finished posting messages. Retrieving all messages...")
let allMessages = server.getMessages()
for message in allMessages {
print("[\(message.timestamp)] \(message.user): \(message.content)")
}
print("All messages printed.")
}
}
Вы увидите сообщения, отправленные не по порядку, а некоторые даже потеряются, поскольку несколько потоков одновременно изменяют массив.
… [2025-04-17 19:20:06 +0000] Bob: Message 98 from Bob [2025-04-17 19:20:06 +0000] Bob: Message 99 from Bob [2025-04-17 19:20:06 +0000] Alice: Message 99 from Alice [2025-04-17 19:20:06 +0000] Diana: Message 94 from Diana [2025-04-17 19:20:06 +0000] Bob: Message 100 from Bob [2025-04-17 19:20:06 +0000] Alice: Message 100 from Alice [2025-04-17 19:20:06 +0000] Diana: Message 95 from Diana [2025-04-17 19:20:06 +0000] Diana: Message 96 from Diana [2025-04-17 19:20:06 +0000] Diana: Message 97 from Diana [2025-04-17 19:20:06 +0000] Diana: Message 98 from Diana [2025-04-17 19:20:06 +0000] Diana: Message 99 from Diana [2025-04-17 19:20:06 +0000] Diana: Message 100 from Diana All messages printed.
Когда следует использовать акторы
Используйте акторы, когда вашему приложению необходимо обрабатывать общие данные для нескольких параллельных задач. Они особенно полезны в высокоасинхронных средах, где менеджеры на основе классов можно заменить акторами, которые безопасно изменяют состояние.
Почему бы не использовать структуры? Структуры — это типы значений, и их копирование позволяет избежать общего состояния. Настоящие проблемы с параллельным доступом возникают со ссылочными типами, такими как классы. Вот почему акторы так важны.
Планируете ли вы использовать акторы в своём следующем проекте? Дайте мне знать в комментариях. Хорошего программирования!

