Когда говорят о параллелизме в
Swift Concurrency, первая ассоциация -
Task { }. Удобно, знакомо, работает. Но если смотреть на задачу только как на способ утащить тяжелые вычисления в фон, легко пропустить главное:
Task - это универсальный инструмент, который умеет быть и будущим результатом, и синхронизатором, и асинхронным контекстом. Просто большинство используют лишь малую часть его возможностей.
Важные возможностей задачи:
- Когда вы сохраняете дескриптор задачи в переменную и передаете ее в другую часть приложения, вы отдаете не сам процесс, а контракт: «рано или поздно здесь появится значение». Именно так работают futures и promises в других языках, но в Swift это встроено в базовый тип. Вы можете ждать результат в любом месте, на любом потоке, без дополнительных примитивов синхронизации.
- Задачи потокобезопасны. Не в том смысле, что код внутри них защищен от гонок, а в том, что доступ к состоянию самой задачи (выполнена, отменена, есть результат) атомарен. Это значит, что одна задача может служить точкой согласования для нескольких независимых асинхронных операций. Первая дошла до результата - остальные просто ждут его на await value. Никаких DispatchGroup, никаких семафоров. Только задача и ее значение.
- Completion-хендлеры, делегаты, колбэки SwiftUI - все они синхронные. Но внутри них часто нужно вызвать асинхронную функцию. Task дает возможность сделать это без хаков: он создает асинхронный контекст внутри синхронного. При этом важно различать намерения. Task.detached раньше использовали для гарантированного фона, но он сбрасывает @TaskLocal - неочевидное и часто нежелательное поведение. В Swift 6.2 появился более чистый способ: Task { @concurrent in }. Семантика та же, но локальный контекст сохраняется.
Почему это важно:
Пока вы мыслите задачами как «замыканиями, которые бегут в фоне», вы пишете больше кода, чем нужно. Каждый раз, когда требуется общий результат или координация, вы тянете в проект
Actor,
Publisher или внешнюю очередь. Но часто достаточно просто передать дескриптор задачи.
Речь не об оптимизации. Это смещение фокуса: вместо «как распараллелить» нужно думать «как описать зависимость».
Task не про потоки, а про значения, которые появятся в будущем. И этот будущий факт можно передавать, как любую другую сущность языка.
Вывод:
Task в
Swift - пример хорошей абстракции, которая скрывает сложность за простым интерфейсом. Ее можно использовать как фоновый исполнитель, как будущий результат, как точку синхронизации и как вход в асинхронность. И все это - один тип, без подключения дополнительных библиотек. Чем лучше вы понимаете эти роли, тем меньше кода пишете и тем меньше состояний нужно синхронизировать вручную.