У меня есть следующий код, и я не уверен, безопасен ли он:
extension Timer {
@MainActor // <- 1
static func myScheduled(
interval: TimeInterval,
block: @escaping @MainActor @Sendable () -> Void) -> Timer // <- 2
{
return scheduledTimer(withTimeInterval: interval, repeats: true) { _ in
MainActor.assumeIsolated(block) // <- 3
}
}
}
В приведенном выше коде первая аннотация @MainActor
(отметка 1 в коде) гарантирует, что функция myScheduled
должна вызываться из изолированного контекста основного актера, а это означает, что Timer.scheduledTimer
включит таймер RunLoop.main
, а это означает, что должен быть вызван обратный вызов таймера. основная нить.
Моему block
нужно вызвать какой-то API для главного актера, поэтому мне нужно пометить его как главного актера (отметка 2 в коде). Однако, поскольку компилятор не знает, что обратный вызов таймера будет выполняться в основном потоке, мне придется использовать здесь MainActor.assumeIsolated(block)
(отметка 3 в коде), чтобы отключить предупреждение компилятора.
Мой вопрос: всегда ли безопасно делать такое предположение? Я точно знаю, что обратный вызов таймера происходит в основном потоке, однако я не уверен, всегда ли безопасно предполагать, что контекст основного потока должен быть изолирован от основного.
Из документа API MainActor.assumeIsolated
:
Этот метод позволяет предположить и проверить, что выполняющаяся в данный момент синхронная функция действительно выполняется на последовательном исполнителе MainActor.
Эта проверка выполняется против последовательного исполнителя MainActor, что означает, что / если другой актер использует тот же последовательный исполнитель (используя SharedUnownedExecutor в качестве своего собственного unownedExecutor), эта проверка будет успешной, поскольку с точки зрения безопасности параллелизма последовательный исполнитель гарантирует взаимное исключение этих два актера.
Другими словами, если что-то выполняется в основном потоке, уверены ли мы, что это должно быть выполнено «исполнителем» этого главного актера?
Если это полезно, основной поток != основная очередь != главный актер. Например, из обсуждения здесь: Блок асинхронного завершения iOS не вызывается, мы знаем, что viewDidLoad
находится в основном потоке, но не в основной очереди. Кроме того, UIViewController
уже помечен как главный актер, что означает, что viewDidLoad
находится в основном потоке + главный актер изолирован, но не в основной очереди.
@Rob Я удалил его, потому что начал сомневаться в себе после обсуждения циклов выполнения и очередей отправки. Исполнителем главного актера является основная очередь, но таймер срабатывает из основного цикла выполнения, и я не был уверен, как связать эти два процесса вместе. Если исполнителем главного актера является основной поток, это было бы легко объяснить, но, насколько я понимаю, это потребует реализации пользовательских исполнителей, как упоминалось в SE-0316.
@Rob ОП тоже хочет установить fireDate
и, возможно, сделать что-то еще с таймером. AsyncTimerSequence
поддерживает это? Я не знаком с пакетом.
@Sweeper – Re fireDate
, у вас есть все стандартные трюки с последовательностями, например stackoverflow.com/a/77416967/1271826. Это просто зависит. Но я не обязательно пытался пропагандировать это в Timer
, а просто указывал на это как на вариант при рассмотрении шаблонов, охватывающих параллелизм Swift.
@Sweeper – Повторите детали цикла запуска/очереди/исполнителя, да, я вас слышу. Я недостаточно углубился в реализацию, чтобы говорить об этом, поэтому ограничил свой комментарий словами «потому что Apple так сказала». ржу не могу.
@Rob «если у вас есть устаревший API в основном потоке, вы можете использовать этот шаблон MainActor.assumeIsolated». Означает ли это, что основной поток эквивалентен основному изолированному?
Может быть, было бы слишком сильно объявить их «эквивалентными» (поскольку один требует от разработчика подтверждения использования основного потока и вводит проверки во время выполнения, тогда как другой обеспечивает статическую безопасность гонок данных во время компиляции), но да, Если вы используете синхронную функцию, которая, как вы знаете, работает в основном потоке, вы можете насладиться изоляцией основного актера, выполнив дополнительный шаг по соединению его с параллелизмом Swift с помощью MainActor.assumeIsolated
.
В SE-0424 Apple сообщает нам:
Возможность таким образом утверждать изоляцию для кода, не связанного с задачей, настолько важна, что среда выполнения Swift фактически уже имеет для нее особый случай: даже если текущий поток не выполняет задачу, проверка изоляции [например,
MainActor.assumeIsolated
] завершится успешно, если целевой актер — этоMainActor
, а текущий поток — основной поток.
Видео Перенесите свое приложение на Swift 6 демонстрирует пример этого. В этом примере в основном потоке вызывается какой-то случайный метод делегата, но здесь справедливо то же самое. Короче говоря, если у вас есть устаревший API в основном потоке, вы можете использовать этот шаблон MainActor.assumeIsolated
.
При использовании MainActor.assumeIsolated
(представленного в Swift 5.9 в SE-0392 ) именно разработчик гарантирует, что он находится в основном потоке, и исполняемый файл будет выполнять проверки во время выполнения, а не наслаждаться статической безопасностью в гонке данных. Но в тех случаях, когда API не перешел на параллелизм Swift (как предусмотрено в SE-0337), но вы точно знаете, что он работает в основном потоке, это краткосрочный обходной путь.
Кстати, SE-0431 , SE-0414 и SE-0423 предоставляют дополнительные обсуждения относительно assumeIsolated
, но, по общему признанию, не имеют прямого отношения к рассматриваемому вопросу.
Кстати, я бы рассмотрел возможность использования AsyncTimerSequence вместо
Timer
, тем самым разрубая гордиев узел.