Connect with us

Программирование

Акторы в Swift — для чего они нужны?

Актор — это ссылочный тип, подобный классу, созданный для безопасного управления изменяемым состоянием в многопоточной среде.

Опубликовано

/

     
     

Актор — это ссылочный тип, подобный классу, созданный для безопасного управления изменяемым состоянием в многопоточной среде. Появившиеся в 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.

Когда следует использовать акторы

Используйте акторы, когда вашему приложению необходимо обрабатывать общие данные для нескольких параллельных задач. Они особенно полезны в высокоасинхронных средах, где менеджеры на основе классов можно заменить акторами, которые безопасно изменяют состояние.

Почему бы не использовать структуры? Структуры — это типы значений, и их копирование позволяет избежать общего состояния. Настоящие проблемы с параллельным доступом возникают со ссылочными типами, такими как классы. Вот почему акторы так важны.

Планируете ли вы использовать акторы в своём следующем проекте? Дайте мне знать в комментариях. Хорошего программирования!

Источник

Если вы нашли опечатку - выделите ее и нажмите Ctrl + Enter! Для связи с нами вы можете использовать info@apptractor.ru.
Telegram

Популярное

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: