У меня есть класс 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, все будет работать так, как будто оно синхронно.
init должен быть асинхронным, чтобы это работало. Поскольку вы не хотите @MainActor, код, не работающий на главном актере, может вызывать init, и тогда должен быть переход актера. Я знаю, что это просто переносит проблему на место вызова, но где-то должен быть этот асинхронный бит.
@loremipsum Мне нужна обратная совместимость с @objc, поэтому я не уверен, что actor может здесь работать. Я подумал, что в этом смысл использования @unchecked Sendable, чтобы компилятор знал, что я обрабатываю логику синхронизации?
Не знаю, что касается цели c, обработка логики синхронизации - это не то же самое, что синхронизация log сообщает async/await: «Я закончил, продолжайте двигаться», даже если DispatchQueue все еще может ждать завершения работы. Вы можете добавить обработчик завершения, а затем преобразовать этот обработчик завершения в асинхронное ожидание, чтобы вы могли «continuation.resume()», когда DispatchQueue действительно будет выполнено.
Идея состоит в том, что сайт вызова действительно может продолжаться. LogManager добавит журнал в массив в свое время, но в sync, чтобы все они вводились в том порядке, в котором они были запущены.
Если вы можете, вы можете сделать initasync таким образом, чтобы журнал действительно вызывался init против init, тогда Task





Я не хочу, чтобы мой 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
Действительно. Но на вашем месте я бы не стал доверять каким-либо обратным портам async/await. Наша команда, поддерживающая приложение для огромной корпорации с несколькими сотнями тысяч пользователей, без угрызений совести прекратила поддержку iOS 14 — и именно в этот момент мы с большим облегчением перешли на async/await (а также как и многие другие инновации iOS 15, такие как средства форматирования нового стиля и строки с атрибутами). Это было год назад; когда выйдет iOS 18, мы, скорее всего, откажемся от iOS 15. Я думаю, что поддержка iOS 12 на данный момент — это просто безумие.
Кстати, обратите внимание, хотя я и не подчеркивал это в своем ответе, моя версия LogManager может использовать Sendable без предупреждений/ошибок.
Когда вы отправляете несколько неструктурированных вызовов параллелизма с помощью Task {…}, они обычно отображаются в том порядке, в котором они были отправлены, но это не гарантируется. Как говорит SE-0306: «Это концептуально похоже на сериал DispatchQueue, но с важным отличием: выполнение задач, ожидающих актера, не гарантируется в том же порядке, в котором они изначально ожидали этого актера». Вы не можете полагаться на актеров и неструктурированный параллелизм для обеспечения порядка выполнения.
Если вы хотите, чтобы сообщения журнала отображались в правильном порядке, используйте свою очередь, например:
@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
}
}
Используя очередь, вам гарантирован порядок выполнения.
Несколько наблюдений:
Нет причин, по которым ваша функция log должна выполняться синхронно с вашей serialQueue. Вы ничего не возвращаете, поэтому sync не нужен и только снижает эффективность. Сейчас это, возможно, не имеет большого значения, но давайте представим, что ваш регистратор тоже записывает информацию в постоянное хранилище; вы не хотели бы существенно влиять на производительность вашего приложения из-за вашей системы журналирования.
dispatchPrecondition не требуется, но хорошей практикой является, чтобы метод, зависящий от конкретной очереди, явно указывал это требование. Таким образом, отладочные сборки предупредят вас о неправильном использовании. Это закрытый метод, поэтому мы знаем, что потенциальное неправильное использование ограничено этим классом, но это по-прежнему разумный метод защитного программирования при написании кода GCD. Это NOOP в сборках релизов, поэтому у защитного программирования нет недостатков.
Не имеет отношения к рассматриваемому вопросу, но соглашение для синглтонов в Swift — shared, поэтому я переименовал его соответствующим образом. Но я также указал @objc(sharedInstance), чтобы вызывающие объекты Objective-C пользовались соглашениями об именах Objective-C.
Но если вы действительно хотите сохранить версию Swift под названием sharedInstance, это ваш выбор. Но просто поймите, что это не стандартное соглашение.
Кроме того, вы можете рассмотреть возможность полного отказа от этого специального менеджера журналирования и использования встроенной системы Unified Logging . В устаревших кодовых базах мы бы использовали OSLog . Я знаю, что это неприменимо к вашему случаю, но в iOS 14 и более поздних версиях можно использовать Logger вместо OSLog.
Система «Единое журналирование» предлагает множество преимуществ по сравнению с пользовательскими системами журналирования:
Вы можете щелкнуть правой кнопкой мыши сообщение журнала и перейти непосредственно к источнику, сгенерировавшем журнал в Xcode 15+.
При запуске приложения на устройстве вы можете отслеживать сообщения журнала в консольном приложении macOS. В целом это полезно, но бесценно при мониторинге фоновых задач, отслеживании журналов на устройстве и т. д.
Как в Xcode, так и в консоли macOS вы можете классифицировать сообщения журнала как сообщения отладки и сообщения об ошибках и соответствующим образом фильтровать.
Вы даже можете загрузить сообщения журнала на устройство постфактум и просмотреть их в приложении macOS Console:
$ log collect --device --start '2024-06-12 08:00:00' —output Foo.logarchive
$ open Foo.logarchive
Поначалу это покажется обременительным (особенно если вы застряли на OSLog для поддержки старых ОС, а не на значительно улучшенной Logger iOS 14+), но преимущества неоспоримы, и как только вы начнете его использовать, вы не будете выглядеть назад. Обязательно рассмотрите «Единое журналирование».
Спасибо за советы и предложения. Я использую этот регистратор, чтобы пользователи присылали мне подробные журналы по электронной почте в случае возникновения проблем. Можем ли мы сделать это с помощью OSLog и Logger?
К сожалению, ваш код выдает предупреждение о параллелизме при доступе к SysInfo.sysInfo(), хотя он отправляется в основную очередь. Есть ли способ очистить это предупреждение (ошибка в Swift 6)?
Это интересно, потому что я, если для параметра «Strict Concurrency Checking» установлено значение «Complete», я не получаю такого предупреждения. Интересно, есть ли какие-то другие изменения, которыми вы с нами не поделились (например, сохранение этого DispatchQueue.main в переменной). В любом случае, чтобы сообщить компилятору, что вы отправили это в основную очередь и, следовательно, главный актер изолирован, просто добавьте MainActor.assumeIsolated {…}, как показано здесь.
Я создал новый проект iOS, вставил приведенный выше код в новый LogManager файл, а затем добавил свой SysInfo класс сверху в конец файла. Изменены предупреждения о параллельном выполнении, и они выдают предупреждение.
В последней версии Xcode со Swift 6 нет предупреждений, так что все в порядке.
Странно, потому что, опять же, я тестировал как в Xcode 15.4, так и в Xcode 16 (бета-версия 1) в режимах Swift 5 и Swift 6. Но assumeIsolated вот так суть — это способ отключить такого рода предупреждения, сохраняя при этом безопасность параллелизма.
@Даррен, я пропустил ваш вопрос: «Я использую этот регистратор, чтобы пользователи присылали мне подробные журналы по электронной почте в случае возникновения проблем. Можем ли мы сделать это с помощью OSLog и Logger?» … Да, я считаю, что вы можете использовать OSLogStore именно для этого.
Не смешивайте DispatchQueue и async/await — это первое правило. Задача запустится после завершения инициализации и до всего остального. Таким образом, значения по умолчанию будут работать, но DispatchQueue несовместим с async/await. Сделайте LogManager актером и избавьтесь от всех GCD.