Как получить доступ к статическому свойству Swift 6, безопасному для параллелизма?

Мое приложение использует класс A и его подклассы SubA_x. A имеет статическое свойство serialNumber, которое изменяется классом A только при инициализации подкласса. Он устанавливает свойство let name, чтобы каждый подкласс имел уникальное имя. Вот код:

class A {
    static var serialNumber = 0
    let name: String

    init( /* some parameters */ ) {
        A.serialNumber += 1
        self.name = "\(A.serialNumber)"
    }
}

final class SubA_1: A {
    init( /* some parameters */ ) {
        super.init( /* some parameters */ )
    }
}

При строгой проверке параллелизма в Swift 6 строка, в которой инициализируется serialNumber, выдает ошибку.

Статическое свойство «serialNumber» не является безопасным для параллелизма, поскольку оно неизолированное глобальное общее изменяемое состояние

Я понимаю, что каждый подкласс A может модифицировать var serialNumber из любого потока, поэтому возможны гонки данных.

Но как мне реализовать эту функциональность безопасным для параллелизма способом?

Я попытался предоставить serialNumber актера:

actor SerialNumberManager {
    private var serialNumber = 0
    
    func getNextSerialNumber() -> Int {
        serialNumber += 1
        return serialNumber
    }
}

но тогда я не могу вызвать getNextSerialNumber() в init, кроме случаев, когда init является асинхронным. Но тогда я смогу инициализировать подкласс только в асинхронном контексте и т. д.

Вероятно, я мог бы обеспечить собственную синхронизацию на основе GCD, но должен быть способ сделать это в рамках параллелизма Swift.

«Но тогда я могу инициализировать подкласс только в асинхронном контексте», и почему это не так? Это цена, которую вы должны заплатить за безопасность.

Sweeper 03.07.2024 14:17

@Подметальная машина Ты прав. Я рефакторинг существующего кода и наивно думал, что смогу обойтись минимальными изменениями. Однако теперь я знаю, что это невозможно с использованием непотокобезопасного кода.

Reinhard Männer 03.07.2024 15:34
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
2
174
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Если вам нужно потокобезопасное общее состояние, логичным подходом будет использование actor. Однако если вам нужен такой, который можно вызывать из синхронных контекстов, вы можете просто написать свой собственный менеджер, реализуя собственную ручную синхронизацию (либо с помощью последовательной очереди GCD, либо, как показано ниже, с блокировкой):

class SerialNumberManager: @unchecked Sendable {
    static let shared = SerialNumberManager()

    private let lock = NSLock()
    private var serialNumber = 0

    private init() { }

    func nextSerialNumber() -> Int {
        lock.withLock {
            serialNumber += 1
            return serialNumber
        }
    }
}

Обратите внимание: мы будем использовать @unchecked Sendable только тогда, когда реализуем ручную синхронизацию, как указано выше.

И вы можете использовать его так:

class A {
    let serialNumber = SerialNumberManager.shared.nextSerialNumber()
    let name: String

    init( /* some parameters */ ) {
        self.name = "\(serialNumber)"
    }
}

В качестве альтернативы вы можете использовать OSAllocatedUnfairLock, который уже есть Sendable:

import os.lock

class A {
    private static let serialNumber = OSAllocatedUnfairLock(initialState: 0)
    let name: String

    init( /* some parameters */ ) {
        let value = Self.serialNumber.withLock { value in
            value += 1
            return value
        }
        self.name = "\(value)"
    }
}

Для полноты картины другие альтернативы включают атомы или UUID.

Другие вопросы по теме

Инициализация статического поля шаблона класса разные результаты в GCC и Clang
Является ли доступ к глобальной «статической» переменной из одного TU гарантированно безопасным?
Является ли `static const` когда-либо лучше/требуется с точки зрения корректности/UB по сравнению с просто `static`?
Как фреймворк Apple использует структуры со своими статическими свойствами
Утечка памяти с использованием статического члена unique_ptr
Как выглядит подвеска JavaScript реализации класса Python?
Почему в Swift инициализация ленивых статических переменных потокобезопасна? Как это реализовано?
Статическое веб-приложение Azure перенаправляет поддомен на внутренний путь
MAUI C# – найти представление по имени в статическом методе
Как определить, когда удобнее использовать статические методы вместо экземплярных? Инкапсуляция является предпочтительным выбором?