Рекомендации по принятию Swift Concurrency
Данный документ пытается предоставить набор рекомендаций для авторов серверных Swift библиотек. В частности, здесь много дискуссий о том, как поступить с существующими API и библиотеками, которые широко используют EventLoopFuture и связанные типы Swift NIO.
Swift Concurrency — это проект, реализуемый на протяжении многих лет. Для сообщества серверных разработчиков очень важно участвовать в этом многолетнем процессе адаптации функций конкурентного программирования, по одной, и предоставлять обратную связь в процессе. Поэтому не стоит откладывать принятие функций конкурентного программирования до Swift 6, так как мы можем упустить ценную возможность улучшить модель конкурентного программирования.
В 2021 году со Swift 5.5 появились структурированное конкурентное программирование и акторы. Сейчас отличное время, чтобы предоставить API, использующие эти примитивы. В будущем мы увидим полностью проверенное конкурентное программирование Swift. Это будет сопровождаться изменениями, несовместимыми со старыми версиями. По этой причине принятие новых функций конкурентного программирования можно разделить на два этапа.
Что можно сделать прямо сейчас
Проектирование API
Во-первых, существующие библиотеки должны стремиться добавлять функции async, где это возможно, в свои пользовательские API, дополнительно к существующим API, основанным на *Future, где это возможно. Эти дополнительные API можно ограничить по версии Swift и добавить без нарушения существующего кода пользователей, например, так:
extension Worker {
func work() -> EventLoopFuture<Value> { ... }
#if compiler(>=5.5) && canImport(_Concurrency)
@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
func work() async throws -> Value { ... }
#endif
}
Если функция не может завершиться ошибкой, но раньше использовала futures, она не должна включать ключевое слово throws в своем новом воплощении.
Такое принятие можно начать немедленно, и это не должно вызвать проблем для существующих пользователей существующих библиотек.
Вспомогательные функции SwiftNIO
Для облегчения перехода к асинхронному коду SwiftNIO предлагает ряд вспомогательных методов для EventLoopFuture и -Promise.
Для каждого EventLoopFuture можно вызвать .get(), чтобы перевести future в вызов, подразумевающий await. Если вы хотите перевести асинхронные/ожидающие вызовы в EventLoopFuture, мы рекомендуем следующую схему:
#if compiler(>=5.5) && canImport(_Concurrency)
func yourAsyncFunctionConvertedToAFuture(on eventLoop: EventLoop)
-> EventLoopFuture<Result> {
let promise = context.eventLoop.makePromise(of: Out.self)
promise.completeWithTask {
try await yourMethod(yourInputs)
}
return promise.futureResult
}
#endif
Существуют и другие вспомогательные функции для EventLoopGroup, Channel, ChannelOutboundInvoker и ChannelPipeline.
#if контроль кода с использованием Concurrency #if guarding code using Concurrency section" href="#if-guarding-code-using-concurrency">
Для того, чтобы иметь код, использующий конкурентное программирование наряду с кодом, его не использующим, вам, возможно, придется #if контролировать определенные части кода. Правильный способ сделать это — следующий:
#if compiler(>=5.5) && canImport(_Concurrency)
...
#endif
Обратите внимание, что вам не нужно импортировать _Concurrency вообще, если оно присутствует, оно импортируется автоматически.
#if compiler(>=5.5) && canImport(_Concurrency)
// DO NOT DO THIS.
// Instead don't do any import and it'll import automatically when possible.
import _Concurrency
#endif
Проверка Sendable
SE-0302 представил протокол
Sendable, который используется для указания типов значений, которые можно безопасно копировать между акторами или, более широко, в любой контекст, где копия значения может использоваться одновременно с оригиналом. Применимый повсеместно к всему Swift коду, проверкаSendableустраняет большой класс гонок данных, вызванных совместным использованием изменяемого состояния.– из Этапа разработки проверки Sendable, который описывает план адаптации
Sendableдля Swift 6.
В будущем мы увидим полностью проверенное конкурентное программирование Swift. Языковые средства для поддержки этого — протокол Sendable и ключевое слово @Sendable для замыканий. Поскольку проверка sendable будет нарушать существующий Swift-код, для этого требуется новая основная версия Swift.
Для облегчения перехода к полностью проверенному Swift-коду сегодня можно аннотировать свои API протоколом Sendable.
Вы можете начать адаптацию Sendable и получать соответствующие предупреждения в Swift 5.5, передав флаг -warn-concurrency, вы можете сделать это в SwiftPM для всего проекта следующим образом:
swift build -Xswiftc -Xfrontend -Xswiftc -warn-concurrency
Проверка Sendable сегодня
Проверка sendable в настоящее время отключена в Swift 5.5(.0), потому что она создавала ряд сложных ситуаций, для решения которых у нас не было инструментов.
Большинство из этих проблем были решены на сегодняшней ветке main компилятора и ожидается, что они появятся в следующих выпусках Swift 5.5. Возможно, стоит подождать с адаптацией до следующей версии(ей) после 5.5.0.
Например, одна из таких возможностей — возможность кортежей типов Sendable соответствовать протоколу Sendable тоже. Мы рекомендуем отложить принятие Sendable, пока этот исправление не появится в Swift 5.5 (что должно произойти довольно скоро). С этим изменением разница между Swift 5.5 с включённой -warn-concurrency и режимом Swift 6 должна быть очень небольшой и управляемой в каждом конкретном случае.
Обратная совместимость объявлений и «проверенного» Swift Concurrency
Принятие Swift Concurrency постепенно будет вызывать больше предупреждений и, в конечном итоге, ошибки компиляции в Swift 6, когда нарушаются проверки sendability, маркируя потенциально небезопасный код.
Библиотеке может быть трудно поддерживать версию, совместимую с версиями до Swift 6, одновременно полностью принимая новые проверки конкурентного программирования. Например, может потребоваться пометить обобщённые типы как Sendable, как показано ниже:
struct Container<Value: Sendable>: Sendable { ... }
Здесь, тип Value должен быть помечен как Sendable, чтобы проверки конкурентного программирования Swift 6 корректно работали с таким контейнером. Однако, поскольку тип Sendable не существует до Swift 5.5, было бы сложно поддерживать библиотеку, которая поддерживает как Swift 5.4+, так и Swift 6.
В таких ситуациях может быть полезно использовать следующий трюк, чтобы иметь возможность использовать одно и то же объявление Container как между версиями Swift библиотеки:
#if swift(>=5.5) && canImport(_Concurrency)
public typealias MYPREFIX_Sendable = Swift.Sendable
#else
public typealias MYPREFIX_Sendable = Any
#endif
ПРИМЕЧАНИЕ: Да, мы здесь используем
swift(>=5.5), в то время как мы используемcompiler(>=5.5)для контроля конкретных API, использующих функции конкурентного программирования.
Псевдоним Any фактически является бесполезным, когда применяется как ограничение обобщения, и таким образом, таким образом, можно сохранить то же объявление Container<Value> работающим между версиями Swift.
Локальные значения задачи и ведение журнала
Новый API локальных значений задачи (SE-0311) позволяет неявно переносить метаданные вместе с выполнением Task. Это естественно подходит для отслеживания и переноса метаданных вместе с выполнением задачи, например, включения их в сообщения журнала.
Мы работаем над тем, чтобы адаптировать SwiftLog, чтобы сделать его достаточно мощным, чтобы автоматически собирать и регистрировать конкретные локальные значения задачи. Это изменение будет внесено совместимым способом.
Сейчас библиотеки должны продолжать использовать метаданные логгера, но мы ожидаем, что в будущем многие случаи, когда метаданные вручную передаются в каждый оператор записи в журнал, можно будет заменить установкой локальных значений задачи.
Подготовка к концепции сроков
Сроки — это ещё одна функция, которая тесно связана с Swift Concurrency и изначально была предложена на ранних этапах предложения Structured Concurrency, а затем перемещена из него. Команда Swift по-прежнему заинтересована в введении концепции сроков в язык, и для этого уже была проведена определённая подготовка в рамках среды выполнения конкурентного программирования. Однако в настоящее время поддержка сроков в Swift Concurrency отсутствует, и вполне допустимо продолжать использовать механизмы, такие как NIODeadline или аналогичные механизмы, для отмены задач после истечения определенного промежутка времени.
После того, как Swift Concurrency получит поддержку крайних сроков, они проявятся в возможности отмены задачи (и её дочерних задач) после превышения такого крайнего срока (момента времени). Чтобы API были «готовыми к крайним срокам», им не нужно делать ничего особенного, кроме подготовки к обработке крайних сроков и их отмены.
Совместная обработка отмены Task
Task отмена существует сегодня в Swift Concurrency и является чем-то, что библиотеки уже могут обрабатывать. На практике это означает, что любая асинхронная функция (или функция, которая ожидается быть вызванной изнутри Task), может использовать API Task.isCancelled или try Task.checkCancellation(), чтобы проверить, была ли отменена задача, в которой она выполняется, и в случае отмены может сотрудничать, чтобы прервать любую операцию, которую она в данный момент выполняет.
Отмена может быть полезной при выполнении длительных операций или перед запуском какой-либо дорогостоящей операции. Например, клиент HTTP МОЖЕТ проверить отмену перед отправкой запроса — возможно, не имеет смысла отправлять запрос, если известно, что задача, ожидающая результата, больше не заинтересована в нём!
Отмену в целом можно понять как «ожидающий результат этой задачи больше не заинтересован в нём», и обычно лучше всего бросить ошибку «отменено», когда встречается отмена. Однако в некоторых ситуациях также может быть уместно вернуть «частичный» результат (например, если задача собирает много результатов, она может вернуть те, которые ей удалось собрать до сих пор, а не возвращать ничего или игнорировать отмену и собирать все оставшиеся результаты).
Что ожидать со Swift 6
Sendable: Глобальные переменные и импортированный код
Сегодня Swift 5.5 ещё не обрабатывает глобальные переменные в своей модели проверки конкурентности. Это скоро изменится, но точные семантики пока не определены. В общем случае, избегайте использования глобальных свойств и переменных, насколько это возможно, чтобы избежать проблем в будущем. По возможности, рассмотрите возможность удаления глобальных переменных.
Некоторые глобальные переменные имеют особые свойства, такие как errno, содержащий код ошибки системных вызовов. Это переменная, локальная для потока, и поэтому её безопасно читать из любого потока/Task. Ожидается, что импортер улучшится, чтобы аннотировать такие глобальные переменные какой-то аннотацией «известно, что безопасно», таким образом, Swift-код, использующий её, даже в полностью проверенном режиме конкурентности, не будет жаловаться на неё. Тем не менее, использование errno и других API «локальных для потока» очень подвержено ошибкам в Swift Concurrency, потому что переходы между потоками могут происходить в любой точке приостановки, поэтому следующий фрагмент кода, скорее всего, неверен:
sys_call(...)
await ...
let err = errno // BAD, we are most likely on a different thread here (!)
Обращайте внимание при взаимодействии с любым API локального для потока из Swift Concurrency. Если ваша библиотека ранее использовала локальное хранилище потоков, вам нужно будет перейти к использованию локальных значений задачи вместо этого, так как они работают правильно с задачами структурированной конкурентности Swift.
Ещё одна сложная ситуация связана с импортированным кодом C. Возможно, нет хорошего способа аннотировать импортированные типы как Sendable (или это будет слишком сложно сделать вручную). Swift, скорее всего, получит улучшенную поддержку импортированного кода и потенциально позволит пропустить некоторые проверки безопасности конкурентности для импортированного кода.
Эти ослабленные семантики для импортированного кода ещё не реализованы, но имейте это в виду, работая с C-API из Swift и пытаясь принять -warn-concurrency режим сегодня. Пожалуйста, сообщите о любых проблемах на bugs.swift.org, чтобы мы могли проинформировать разработку этих эвристик проверки, основанных на реальных проблемах, с которыми вы столкнулись.
Пользовательские исполнители
Ожидается, что Swift Concurrency позволит использовать пользовательские исполнители в будущем. Пользовательский исполнитель позволит запускать акторы/задачи «в» таком исполнителе. Возможно, EventLoop станут такими исполнителями, однако предложение о пользовательских исполнителях ещё не было выдвинуто.
Хотя мы ожидаем потенциального прироста производительности при использовании пользовательских исполнителей «в том же цикле событий» за счёт избегания асинхронных переходов между вызовами различных акторов, их появление не изменит фундаментально структуру библиотек NIO.
Данное руководство будет развиваться по мере появления предложений Swift Evolution о пользовательских исполнителях, но не откладывайте принятие Swift Concurrency до того, как пользовательские исполнители «будут реализованы» — важно начать принятие как можно раньше. Для большинства кода мы считаем, что выгоды от принятия Swift Concurrency значительно перевешивают незначительную потерю производительности, которую могут вызвать переходы между акторами.
Сократите использование SwiftNIO Futures как «библиотеки конкурентности»
SwiftNIO в настоящее время предоставляет ряд типов конкурентности для экосистемы Swift on Server. Наиболее заметны EventLoopFuture и EventLoopPromise, которые широко используются для асинхронных результатов. Хотя SSWG ранее рекомендовала использовать их на уровне API для более лёгкого взаимодействия серверных библиотек, мы рекомендуем отказаться или удалить такие API после выхода Swift 6. Экосистема swift-server должна полностью перейти на возможности структурированной конкурентности, которые предоставляет язык. По этой причине крайне важно обеспечить сегодня API async/await, чтобы предоставить пользователям вашей библиотеки время для принятия новых API.
Некоторые типы NIO, однако, останутся в публичных интерфейсах библиотек Swift on server. Ожидается, что сетевые клиенты и серверы будут по-прежнему инициализированы с EventLoopGroup. Однако механизм базового транспорта (NIOPosix и NIOTransportServices) должен стать деталями реализации и не должен быть доступен пользователям библиотек.
SwiftNIO 3
Хотя это может измениться, вероятно, SwiftNIO выпустит версию 3.0 в месяцы после Swift 6.0, когда Swift включит проверку Sendable.
Не ожидайте, что NIO внезапно станет «более асинхронным», основные принципы дизайна NIO заключаются в выполнении небольших задач в цикле событий и использовании Futures для любых асинхронных операций. Дизайн NIO не ожидается к изменению. Канальные конвейеры не ожидается сделать «асинхронными» в смысле Swift Concurrency. Это потому, что SwiftNIO по своей сути — система ввода-вывода, и это создаёт проблему для совместного, общего, используемого пулом потоков Swift Concurrency. Этот пул потоков не должен блокироваться никакой операцией, потому что это приведёт к голоданию пула и предотвратит дальнейшее продвижение других асинхронных задач.
Однако системы ввода-вывода должны в какой-то момент заблокировать поток, ожидая дополнительных событий ввода-вывода, либо в системном вызове ввода-вывода, либо в чём-то вроде epoll_wait. Так работает NIO: каждый из потоков цикла событий в конечном итоге блокируется в epoll_wait. Мы не можем делать это внутри кооперативного пула потоков, так как это привело бы к его голоданию другими асинхронными задачами, поэтому мы должны сделать это в другом потоке. Таким образом, SwiftNIO не должен использоваться в *кооперативном* пуле потоков, а должен взять на себя ответственность и полный контроль над своими потоками — поскольку это система ввода-вывода.
Было бы возможно заставить всю работу NIO работать в кооперативном пуле и переходить между каждой операцией ввода-вывода и передавать её в асинхронный/ожидающий пул, но это неприемлемо для высокопроизводительного ввода-вывода: переключение контекста для *каждой операции ввода-вывода* слишком дорого. В результате SwiftNIO не планирует просто принять Swift Concurrency для упрощения использования, потому что в его конкретном контексте переключения контекста не являются приемлемой компромиссом. SwiftNIO может, однако, сотрудничать с Swift Concurrency с появлением «пользовательских исполнителей» в среде выполнения языка, но это предложение ещё не было полностью выдвинуто, поэтому мы не будем слишком много спекулировать об этом.
Тем не менее, команда NIO воспользуется возможностью удалить устаревшие API и улучшить некоторые API. Область изменений должна быть сопоставима с обновлением версии NIO1 → NIO2. Если ваш код SwiftNIO компилируется сегодня без предупреждений, вероятность того, что он продолжит работать без изменений в NIO3, очень высока.
После выпуска NIO3 NIO2 будет видеть только исправления ошибок.
Разрыв кода конечного пользователя
Ожидается, что Swift 6 вызовет разрыв некоторого кода. Как упоминалось, SwiftNIO 3 также будет выпущен примерно в то же время, что и Swift 6. Учитывая это, может быть хорошей идеей согласовать релизы основных версий в одно время, а также обновить требования к версиям до Swift 6 и NIO 3 в ваших библиотеках.
И Swift, и SwiftNIO не планируют проводить «значительных изменений», поэтому принятие должно быть возможным без больших трудностей.
Рекомендации для пользователей библиотек
Как только выйдет Swift 6, мы рекомендуем использовать самые последние Swift 6 инструментарии, даже если используете режим языка Swift 5.5.n (который может давать только предупреждения вместо жёстких ошибок при проверке Sendability). Это приведёт к лучшим предупреждениям и подсказкам компилятора, чем просто использование инструментария 5.5.
The Swift Programming Language, Copyright © 2014-2025 Apple Inc.
Swift and the Swift logo are trademarks of Apple Inc.
Documentation for Swift 6.0.3
https://www.swift.org/documentation/server/guides/libraries/concurrency-adoption-guidelines.html