Какую гарантию даёт атрибут @Sendable у замыкания, захватывающего ссылочный объект?
@Sendable гарантирует, что замыкание соответствует правилам безопасной передачи между конкурентными задачами: компилятор проверяет его захваты и доступные операции. Этот атрибут не делает захваченный ссылочный объект потокобезопасным и не добавляет ему блокировки или сериализацию.
Если замыкание захватывает изменяемый экземпляр обычного класса, Swift обычно отклоняет такой код при строгой проверке конкурентности. Для безопасного решения объект должен быть неизменяемым и передаваемым, изолированным actor или защищённым другим синхронизирующим механизмом.
До появления встроенной модели конкурентности Swift многие ошибки возникали из-за неявной передачи замыканий и ссылок между потоками: данные изменялись одновременно, а компилятор не мог проверить безопасность таких обращений.
Sendable и @Sendable появились как часть статической модели конкурентности Swift. Тип Sendable описывает безопасную передачу значения, а @Sendable переносит эту проверку на замыкание и его захваченное окружение.
Замыкание может быть создано в одном контексте, а выполнено позже другой задачей или на другом исполнителе. Если оно захватывает ссылочный объект с изменяемым состоянием, несколько выполнений могут обратиться к одному состоянию одновременно.
Обычная ссылка не копирует объект. Поэтому захват ссылки в замыкании не устраняет общий доступ: разные задачи по-прежнему могут менять один экземпляр. Ошибка особенно опасна тем, что простые тесты могут проходить, а гонка проявится только при определённом порядке выполнения.
@Sendable является свойством типа функции. Оно означает, что замыкание предназначено для передачи через границу конкурентности, поэтому его захваты должны удовлетворять правилам безопасной передачи.
Компилятор проверяет прежде всего следующее:
Атрибут не меняет семантику ссылочного типа. Например, экземпляр обычного класса с изменяемым свойством не становится безопасным только потому, что находится внутри @Sendable-замыкания.
При строгой проверке конкурентности такой захват должен быть диагностирован: Counter не объявлен как безопасный для передачи, а изменение его состояния не защищено. Объявление типа как @unchecked Sendable лишь отключает часть проверки и переносит ответственность на разработчика; это не создаёт синхронизацию.
Безопасный вариант — передавать ссылку на actor и обращаться к состоянию через его изолированные методы. Ссылка на actor предназначена для передачи между задачами, а изменение его состояния сериализуется самим actor.
Другой вариант — захватывать неизменяемое значение, действительно соответствующее Sendable, например структуру, состоящую только из безопасно передаваемых полей. Копирование такой структуры обычно исключает общий изменяемый объект, но не делает автоматически безопасными вложенные ссылочные значения.
Сервис загрузки принимает обработчик, который может быть вызван фоновой задачей. Разработчик захватывает в обработчике объект кэша с изменяемым словарём и помечает обработчик как @Sendable.
Первый вариант — оставить обычный класс и подавить предупреждение через @unchecked Sendable. Это быстро, но опасно: при параллельных чтениях и записях словаря гонка остаётся, а компилятор больше не сможет помочь.
Второй вариант — защищать словарь отдельным mutex. Он подходит, если нужны короткие синхронные критические секции и вызов должен завершаться без await, но требует аккуратно определить границы блокировки и не держать её во время долгих операций.
Выбранный вариант — поместить состояние кэша в actor, а обработчику передавать ссылку на него. Это даёт проверяемую границу изоляции и не позволяет случайно менять словарь напрямую. Цена решения — асинхронные обращения к actor и необходимость учитывать возможную приостановку между операциями.
1. Делает ли @Sendable захваченную ссылку неизменяемой?
Нет. let для ссылки запрещает переназначить саму ссылку, но не запрещает менять объект, на который она указывает. Поэтому let-ссылка на обычный изменяемый класс всё ещё может привести к гонке данных.
2. Достаточно ли добавить @unchecked Sendable классу с изменяемыми свойствами?
Нет. Это только обещание компилятору, что разработчик самостоятельно обеспечил безопасность. Нужно реально использовать actor, mutex, неизменяемость или другую корректную схему синхронизации; иначе атрибут скрывает ошибку вместо её исправления.
3. Можно ли захватить actor в @Sendable-замыкании?
Да, ссылка на actor предназначена для передачи между конкурентными задачами. Однако доступ к изолированному состоянию actor всё равно выполняется через его правила изоляции, обычно с await. Сам факт безопасной передачи ссылки не превращает произвольный вызов его методов в синхронный и не разрешает прямое чтение его состояния.