Перевод статьи
Sendable и @Sendable являются частью изменений параллелизма, появившихся в Swift 5.5, и решают сложную проблему проверки типов значений, передаваемых между структурированными конструкциями параллелизма и сообщениями актора.
Прежде чем погрузиться в тему sendables, я рекомендую вам прочитать мои статьи об async/await, actors и actor isolation. Эти статьи охватывают основы новых изменений параллелизма, которые напрямую связаны с методами, описанными в этой статье.
Когда мне следует использовать Sendable?
Протокол Sendable и замыкание указывают, был ли общедоступный API переданных значений передан компилятору потокобезопасным. Общедоступный API безопасно использовать в доменах параллелизма, если нет общедоступных мутаторов, имеется внутренняя система блокировки или мутаторы реализуют копирование при записи, как с типами значений.
Многие типы стандартной библиотеки уже поддерживают протокол Sendable, что устраняет необходимость добавлять соответствие многим типам. В результате поддержки стандартной библиотеки компилятор может неявно создавать поддержку ваших пользовательских типов.
Например, целые числа поддерживают протокол:
extension Int: Sendable {}
Как только мы создадим структуру типа значения с одним свойством типа int, мы неявно получим поддержку протокола Sendable:
// Implicitly conforms to Sendable
struct Article {
var views: Int
}
В то же время следующий пример класса Article не имел бы неявного соответствия:
// Does not implicitly conform to Sendable
class Article {
var views: Int
}
Класс не соответствует требованиям, потому что это ссылочный тип и, следовательно, его можно изменить из других параллельных доменов. Другими словами, Article класс не является потокобезопасной для передачи, и компилятор не может неявно пометить ее как Sendable.
Неявное соответствие при использовании дженериков и перечислений
Полезно понимать, что компилятор не добавляет неявное соответствие к универсальным типам, если универсальный тип не соответствует Sendable.
// No implicit conformance to Sendable because Value does not conform to Sendable
struct Container<Value> {
var child: Value
}
Однако, если мы добавим требование протокола к нашему дженерику значение, мы получим неявную поддержку:
// Container implicitly conforms to Sendable as all its public properties do so too.
struct Container<Value: Sendable> {
var child: Value
}
То же самое относится и к перечислениям со связанными значениями:
Вы можете видеть, что мы автоматически получаем ошибку компилятора:
Связанное значение «loggedIn(name:)» перечисления, соответствующего «Sendable«, «State» имеет non-sendable тип «(name: NSAttributedString)«
Мы можем устранить ошибку, используя вместо этого тип значения String, поскольку он уже соответствует Sendable:
enum State: Sendable {
case loggedOut
case loggedIn(name: String)
}
Генерация ошибок из потокобезопасных экземпляров
Те же правила применяются к ошибкам, которые хотят соответствовать Sendable:
struct ArticleSavingError: Error {
var author: NonFinalAuthor
}
extension ArticleSavingError: Sendable { }
Так как автор не является окончательным и не является потокобезопасным (об этом позже), мы столкнемся со следующей ошибкой:
Сохраненное свойство «author» структуры «ArticleSavingError«, соответствующей «Sendable«, имеет неотправляемый тип «NonFinalAuthor«.
Вы можете решить эту ошибку, убедившись, что все члены ArticleSavingError соответствуют Sendable.
Как использовать протокол Sendable
Неявное соответствие устраняет множество случаев, когда нам нужно самим добавить соответствие протоколу Sendable. Однако бывают случаи, когда компилятор не добавляет неявное соответствие, хотя мы знаем, что наш тип потокобезопасен.
Распространенными примерами типов, которые не могут быть Sendable неявно, но могут быть помечены как таковые, являются неизменяемые классы и классы с внутренними механизмами блокировки:
/// User is immutable and therefore thread-safe, so can conform to Sendable
final class User: Sendable {
let name: String
init(name: String) { self.name = name }
}
Изменяемые классы должны быть отмечены атрибутом @unchecked, чтобы указать, что наш класс на самом деле является потокобезопасным из-за внутренних механизмов блокировки:
extension DispatchQueue {
static let userMutatingLock = DispatchQueue(label: "person.lock.queue")
}
final class MutableUser: @unchecked Sendable {
private var name: String = ""
func updateName(_ name: String) {
DispatchQueue.userMutatingLock.sync {
self.name = name
}
}
}
Ограничение соответствия Sendable в одном и том же исходном файле
Соответствие протоколу Sendable должно происходить в одном и том же исходном файле, чтобы гарантировать, что компилятор проверяет все видимые члены на безопасность потоков.
Например, вы можете определить следующий тип внутри модуля, такого как пакет Swift:
public struct Article {
internal var title: String
}
Сама Article публичная, а заголовок внутренний и не виден за пределами модуля. Таким образом, компилятор не может применить соответствие для Sendable за пределами исходного файла, поскольку он не видит свойство title, даже если заголовок использует тип String для отправки.
Та же проблема возникает при попытке согласования неизменяемого не конечного класса с Sendable:
Поскольку класс не является окончательным, мы не можем соответствовать Sendable, поскольку мы не уверены, будут ли другие классы наследоваться от User с non-Sendable членами. Таким образом, мы столкнулись бы со следующей ошибкой:
Неконечный класс «User» не может соответствовать «Sendable«; используйте «@unchecked Sendable«
Как видите, компилятор предлагает использовать @unchecked Sendable. Мы можем добавить этот атрибут в наш пользовательский экземпляр и избавиться от ошибки:
class User: @unchecked Sendable {
let name: String
init(name: String) { self.name = name }
}
Однако это требует, чтобы мы всегда обеспечивали потокобезопасность всякий раз, когда мы наследуем от User. Поскольку мы накладываем дополнительную ответственность на себя и своих коллег, я бы не рекомендовал использовать этот атрибут вместо использования композиции, финальных классов или типов значений.
Как использовать @Sendable
Функции могут передаваться через домены параллелизма и, следовательно, также требуют соответствия для отправки. Однако функции не могут соответствовать протоколам, поэтому вводится атрибут @Sendable. Примерами функций, которые вы можете передать, являются глобальные объявления функций, замыкания и средства доступа, такие как геттеры и сеттеры.
Часть мотивации SE-302 заключается в том, чтобы выполнять как можно меньше синхронизации:
мы хотим, чтобы подавляющее большинство кода в такой системе было свободным от синхронизации
Используя атрибут @Sendable, мы сообщаем компилятору, что дополнительная синхронизация не требуется, поскольку все захваченные значения в замыкании являются потокобезопасными для работы. Типичным примером может быть использование замыканий внутри изоляции Actor:
actor ArticlesList {
func filteredArticles(_ isIncluded: @Sendable (Article) -> Bool) async -> [Article] {
// ...
}
}
Если бы вы использовали замыкание с non-sendable типом, мы бы столкнулись с ошибкой:
let listOfArticles = ArticlesList()
var searchKeyword: NSAttributedString? = NSAttributedString(string: "keyword")
let filteredArticles = await listOfArticles.filteredArticles { article in
// Error: Reference to captured var 'searchKeyword' in concurrently-executing code
guard let searchKeyword = searchKeyword else { return false }
return article.title == searchKeyword.string
}
Конечно, мы можем быстро решить этот случай, просто используя вместо этого обычную строку, но это демонстрирует, как компилятор помогает нам обеспечить безопасность потоков.
Продолжаем свое путешествие в Swift Concurrency
Изменения параллелизма — это больше, чем просто асинхронное ожидание, они включают в себя множество новых функций, которые вы можете использовать в своем коде. Теперь, когда вы узнали о Sendable, пришло время погрузиться в другие функции параллелизма:
- Tasks in Swift explained with code examples
- Async await in Swift explained with code examples
- Sendable and @Sendable closures explained with code examples
- Nonisolated and isolated keywords: Understanding Actor isolation
- Async let explained: call async functions in parallel
- MainActor usage in Swift explained to dispatch to the main thread
- Actors in Swift: how to use and prevent data races
Заключение
Протокол Sendable и атрибут @Sendable для функций позволяют сообщать компилятору о безопасности потоков при работе с параллелизмом в Swift. Обе функции были введены для достижения более важной цели усилий Swift Concurrency, которая предоставляет механизм для изоляции состояний в параллельных программах для устранения гонок данных. Компилятор поможет нам во многих случаях с неявным соответствием Sendable, но мы всегда можем добавить соответствие сами.
Если вы хотите узнать больше советов по Swift, посетите страницу категории Swift. Если у вас есть какие-либо дополнительные предложения или отзывы, не стесняйтесь обращаться ко мне или писать мне в Твиттере.
Спасибо!