Синхронизация изолированного метода MainActor во время инициализации и обеспечение параллелизма Swift 6

У меня есть класс LogManagersingleton, который сохраняет строки в массив для целей протоколирования. Я хочу, чтобы первая запись в журнале содержала некоторую системную информацию, чтобы она добавлялась во время init

@objc
public final class LogManager: NSObject, @unchecked Sendable {
    
    static let sharedInstance = LogManager()
    private var _logs: [String] = []
    private let serialQueue = DispatchQueue(label: "LoggingSerialQueue")
    
    private override init() {
        super.init()
        
        log(SysInfo.sysInfo())
    }
    
    func log(_ log: String) {
        serialQueue.sync {
            self._logs.append(log)
            print("****LogManager**** \(log)")
        }
    }
}

Проблема в том, что системную информацию необходимо MainActor изолировать, поскольку она вызывает MainActor изолированные свойства UIDevice и некоторых других.

private final class SysInfo {
    
    @MainActor
    static func sysInfo() -> String {
        UIDevice.current.systemName
    }
}

Я не хочу, чтобы мой LogManager был изолирован MainActor, поскольку его можно вызывать из разных потоков.

Я знаю, что могу добавить вызов log от init в Task вот так:

private override init() {
    super.init()
    
    Task {
        await log(SysInfo.sysInfo())
    }
}

но на самом деле это приводит к тому, что это второй журнал в массиве, а не первый, когда я запускаю класс, отправляя команду журнала:

LogManager.sharedInstance.log(#function)

Мне интересно, какой подход здесь лучше всего использовать? До Swift Concurrency, если я удалю MainActor из SysInfo, все будет работать так, как будто оно синхронно.

Не смешивайте DispatchQueue и async/await — это первое правило. Задача запустится после завершения инициализации и до всего остального. Таким образом, значения по умолчанию будут работать, но DispatchQueue несовместим с async/await. Сделайте LogManager актером и избавьтесь от всех GCD.

lorem ipsum 12.06.2024 11:53
init должен быть асинхронным, чтобы это работало. Поскольку вы не хотите @MainActor, код, не работающий на главном актере, может вызывать init, и тогда должен быть переход актера. Я знаю, что это просто переносит проблему на место вызова, но где-то должен быть этот асинхронный бит.
Sweeper 12.06.2024 12:02

@loremipsum Мне нужна обратная совместимость с @objc, поэтому я не уверен, что actor может здесь работать. Я подумал, что в этом смысл использования @unchecked Sendable, чтобы компилятор знал, что я обрабатываю логику синхронизации?

Darren 12.06.2024 12:24

Не знаю, что касается цели c, обработка логики синхронизации - это не то же самое, что синхронизация log сообщает async/await: «Я закончил, продолжайте двигаться», даже если DispatchQueue все еще может ждать завершения работы. Вы можете добавить обработчик завершения, а затем преобразовать этот обработчик завершения в асинхронное ожидание, чтобы вы могли «continuation.resume()», когда DispatchQueue действительно будет выполнено.

lorem ipsum 12.06.2024 13:44

Идея состоит в том, что сайт вызова действительно может продолжаться. LogManager добавит журнал в массив в свое время, но в sync, чтобы все они вводились в том порядке, в котором они были запущены.

Darren 12.06.2024 13:47

Если вы можете, вы можете сделать initasync таким образом, чтобы журнал действительно вызывался init против init, тогда Task

lorem ipsum 12.06.2024 14:13
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
6
186
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Я не хочу, чтобы мой LogManager был изолирован от MainActor, поскольку его можно вызывать из множества разных потоков.

В этом суть дела: вам нужно перестать так думать. Это именно та глупость, которую решает Swift Concurrency.

Избавьтесь от всех объектов DispatchQueue в своем коде (за исключением редких случаев, когда они могут вам понадобиться, чтобы сделать тип @unchecked Sendable действительно потокобезопасным — но на самом деле это не тот случай) и тщательно и последовательно внедряйте Swift Concurrency. . Тогда нет «потоков» и переключение контекста перестает иметь значение. Просто изолируйте LogManager от какого-нибудь актера и перестаньте беспокоиться.

Предположим, просто ради примера, что мы изолируем LogManager от главного актера. Тогда мы можем переписать это так:

@objc @MainActor public final class LogManager: NSObject {

    static let sharedInstance = LogManager()
    private let loggingActor = LoggingActor()

    private override init() {
        super.init()
        Task {
            await loggingActor.log(UIDevice.current.systemName)
        }
    }

    func log(_ what: String) {
        Task {
            await loggingActor.log(what)
        }
    }

    actor LoggingActor {
        private var _logs: [String] = []
        func log(_ log: String) {
            self._logs.append(log)
            print("****LogManager**** \(log)")
        }
    }
}

Теперь, когда мы вызываем LogManager.sharedInstance.log(#function) в didFinishLaunching, мы получаем:

****LogManager**** iOS
****LogManager**** application(_:didFinishLaunchingWithOptions:)

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

    Task {
        await LogManager.sharedInstance.log(#function)
    }

и вы все равно получите все в правильном порядке.

Вот действительно приятная часть. Давайте решим полностью отделить LogManager от главного актера и изолировать его от нашего собственного глобального актера:

@globalActor public actor LoggingGlobalActor {
    public static let shared = LoggingGlobalActor()
}

@objc @LoggingGlobalActor public final class LogManager: NSObject { // ...

Что происходит, когда мы это делаем? Абсолютно ничего! Весь наш код продолжает работать так же, как и раньше. И в этом моя точка зрения: ваше «его можно вызывать из разных потоков» полностью отпадает.

Спасибо. Думаю, я не упомянул другое ограничение: я надеялся сохранить поддержку iOS 12, что означает отсутствие Task использования. Думаю, пришло время отказаться от iOS 12 и, возможно, даже objc

Darren 12.06.2024 15:53

Действительно. Но на вашем месте я бы не стал доверять каким-либо обратным портам async/await. Наша команда, поддерживающая приложение для огромной корпорации с несколькими сотнями тысяч пользователей, без угрызений совести прекратила поддержку iOS 14 — и именно в этот момент мы с большим облегчением перешли на async/await (а также как и многие другие инновации iOS 15, такие как средства форматирования нового стиля и строки с атрибутами). Это было год назад; когда выйдет iOS 18, мы, скорее всего, откажемся от iOS 15. Я думаю, что поддержка iOS 12 на данный момент — это просто безумие.

matt 12.06.2024 16:35

Кстати, обратите внимание, хотя я и не подчеркивал это в своем ответе, моя версия LogManager может использовать Sendable без предупреждений/ошибок.

matt 12.06.2024 16:37

Когда вы отправляете несколько неструктурированных вызовов параллелизма с помощью Task {…}, они обычно отображаются в том порядке, в котором они были отправлены, но это не гарантируется. Как говорит SE-0306: «Это концептуально похоже на сериал DispatchQueue, но с важным отличием: выполнение задач, ожидающих актера, не гарантируется в том же порядке, в котором они изначально ожидали этого актера». Вы не можете полагаться на актеров и неструктурированный параллелизм для обеспечения порядка выполнения.

Rob 12.06.2024 22:47
Ответ принят как подходящий

Если вы хотите, чтобы сообщения журнала отображались в правильном порядке, используйте свою очередь, например:

@objc
public final class LogManager: NSObject, @unchecked Sendable {
    @objc(sharedInstance)
    static let shared = LogManager()

    private var _logs: [String] = []
    private let serialQueue = DispatchQueue(label: "LoggingSerialQueue")

    private override init() {
        super.init()

        serialQueue.async {
            let message = DispatchQueue.main.sync {            // note, we rarely use `sync`, but this is an exception
                SysInfo.sysInfo()
            }
            self.appendToLog(message: message)
        }
    }

    func log(_ message: String) {
        serialQueue.async {                                    // but do not use `sync` here; why make the caller wait?
            self.appendToLog(message: message)
        }
    }
}

private extension LogManager {
    func appendToLog(message: String) {
        dispatchPrecondition(condition: .onQueue(serialQueue)) // make sure caller used correct queue
        _logs.append(message)
        print("****LogManager**** \(message)")
    }
}

private final class SysInfo {
    @MainActor
    static func sysInfo() -> String {
        UIDevice.current.systemName
    }
}

Используя очередь, вам гарантирован порядок выполнения.

Несколько наблюдений:

  1. Нет причин, по которым ваша функция log должна выполняться синхронно с вашей serialQueue. Вы ничего не возвращаете, поэтому sync не нужен и только снижает эффективность. Сейчас это, возможно, не имеет большого значения, но давайте представим, что ваш регистратор тоже записывает информацию в постоянное хранилище; вы не хотели бы существенно влиять на производительность вашего приложения из-за вашей системы журналирования.

  2. dispatchPrecondition не требуется, но хорошей практикой является, чтобы метод, зависящий от конкретной очереди, явно указывал это требование. Таким образом, отладочные сборки предупредят вас о неправильном использовании. Это закрытый метод, поэтому мы знаем, что потенциальное неправильное использование ограничено этим классом, но это по-прежнему разумный метод защитного программирования при написании кода GCD. Это NOOP в сборках релизов, поэтому у защитного программирования нет недостатков.

  3. Не имеет отношения к рассматриваемому вопросу, но соглашение для синглтонов в Swift — shared, поэтому я переименовал его соответствующим образом. Но я также указал @objc(sharedInstance), чтобы вызывающие объекты Objective-C пользовались соглашениями об именах Objective-C.

    Но если вы действительно хотите сохранить версию Swift под названием sharedInstance, это ваш выбор. Но просто поймите, что это не стандартное соглашение.


Кроме того, вы можете рассмотреть возможность полного отказа от этого специального менеджера журналирования и использования встроенной системы Unified Logging . В устаревших кодовых базах мы бы использовали OSLog . Я знаю, что это неприменимо к вашему случаю, но в iOS 14 и более поздних версиях можно использовать Logger вместо OSLog.

Система «Единое журналирование» предлагает множество преимуществ по сравнению с пользовательскими системами журналирования:

  1. Вы можете щелкнуть правой кнопкой мыши сообщение журнала и перейти непосредственно к источнику, сгенерировавшем журнал в Xcode 15+.

  2. При запуске приложения на устройстве вы можете отслеживать сообщения журнала в консольном приложении macOS. В целом это полезно, но бесценно при мониторинге фоновых задач, отслеживании журналов на устройстве и т. д.

  3. Как в Xcode, так и в консоли macOS вы можете классифицировать сообщения журнала как сообщения отладки и сообщения об ошибках и соответствующим образом фильтровать.

  4. Вы даже можете загрузить сообщения журнала на устройство постфактум и просмотреть их в приложении macOS Console:

    $ log collect --device --start '2024-06-12 08:00:00' —output Foo.logarchive
    $ open Foo.logarchive
    

Поначалу это покажется обременительным (особенно если вы застряли на OSLog для поддержки старых ОС, а не на значительно улучшенной Logger iOS 14+), но преимущества неоспоримы, и как только вы начнете его использовать, вы не будете выглядеть назад. Обязательно рассмотрите «Единое журналирование».

Спасибо за советы и предложения. Я использую этот регистратор, чтобы пользователи присылали мне подробные журналы по электронной почте в случае возникновения проблем. Можем ли мы сделать это с помощью OSLog и Logger?

Darren 14.06.2024 16:06

К сожалению, ваш код выдает предупреждение о параллелизме при доступе к SysInfo.sysInfo(), хотя он отправляется в основную очередь. Есть ли способ очистить это предупреждение (ошибка в Swift 6)?

Darren 14.06.2024 16:07

Это интересно, потому что я, если для параметра «Strict Concurrency Checking» установлено значение «Complete», я не получаю такого предупреждения. Интересно, есть ли какие-то другие изменения, которыми вы с нами не поделились (например, сохранение этого DispatchQueue.main в переменной). В любом случае, чтобы сообщить компилятору, что вы отправили это в основную очередь и, следовательно, главный актер изолирован, просто добавьте MainActor.assumeIsolated {…}, как показано здесь.

Rob 14.06.2024 17:41

Я создал новый проект iOS, вставил приведенный выше код в новый LogManager файл, а затем добавил свой SysInfo класс сверху в конец файла. Изменены предупреждения о параллельном выполнении, и они выдают предупреждение.

Darren 14.06.2024 17:56

В последней версии Xcode со Swift 6 нет предупреждений, так что все в порядке.

Darren 14.06.2024 17:58

Странно, потому что, опять же, я тестировал как в Xcode 15.4, так и в Xcode 16 (бета-версия 1) в режимах Swift 5 и Swift 6. Но assumeIsolated вот так суть — это способ отключить такого рода предупреждения, сохраняя при этом безопасность параллелизма.

Rob 14.06.2024 18:09

@Даррен, я пропустил ваш вопрос: «Я использую этот регистратор, чтобы пользователи присылали мне подробные журналы по электронной почте в случае возникновения проблем. Можем ли мы сделать это с помощью OSLog и Logger?» … Да, я считаю, что вы можете использовать OSLogStore именно для этого.

Rob 16.06.2024 22:57

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