Конкурентность
Выполнение асинхронных операций.
Swift имеет встроенную поддержку написания асинхронного и параллельного кода структурированным способом. Асинхронный код может быть приостановлен и возобновлен позже, хотя только один фрагмент программы выполняется в данный момент. Приостановка и возобновление кода в вашей программе позволяет ему продолжать прогресс по краткосрочным операциям, таким как обновление пользовательского интерфейса, одновременно продолжая работу над длительными операциями, такими как получение данных через сеть или разбор файлов. Параллельный код означает, что несколько фрагментов кода выполняются одновременно — например, компьютер с четырёхъядерным процессором может выполнять четыре фрагмента кода одновременно, каждый ядро выполняет одну из задач. Программа, использующая параллельный и асинхронный код, выполняет несколько операций одновременно и приостанавливает операции, ожидающие внешней системы.
Дополнительная гибкость планирования при использовании параллельного или асинхронного кода также влечёт за собой увеличение сложности. Swift позволяет выразить своё намерение таким образом, чтобы можно было выполнить проверку на этапе компиляции — например, вы можете использовать акторы для безопасного доступа к изменяемому состоянию. Однако добавление конкурентности в медленный или ошибочный код не гарантирует, что он станет быстрым или правильным. На самом деле, добавление конкурентности может даже затруднить отладку вашего кода. Однако использование встроенной в Swift поддержки конкурентности в коде, который должен быть конкурентным, означает, что Swift может помочь вам обнаружить проблемы на этапе компиляции.
В остальной части этой главы термин конкурентность используется для обозначения этого распространённого сочетания асинхронного и параллельного кода.
Примечание: Если вы ранее писали конкурентный код, вы, возможно, привыкли работать с потоками. Модель конкурентности в Swift построена на основе потоков, но вы не взаимодействуете с ними напрямую. Асинхронная функция в Swift может отказаться от потока, на котором она выполняется, что позволяет другой асинхронной функции выполняться на этом потоке, в то время как первая функция заблокирована. Когда асинхронная функция возобновляется, Swift не гарантирует, на каком потоке эта функция будет выполняться.
Хотя можно написать конкурентный код без использования поддержки языка Swift, такой код, как правило, сложнее для чтения. Например, следующий код загружает список имён фотографий, загружает первую фотографию в этом списке и отображает эту фотографию пользователю:
listPhotos(inGallery: "Summer Vacation") { photoNames in
let sortedNames = photoNames.sorted()
let name = sortedNames[0]
downloadPhoto(named: name) { photo in
show(photo)
}
} Даже в этом простом случае, так как код должен быть написан как последовательность обработчиков завершения, вы получаете вложенные замыкания. В этом стиле более сложный код с глубоким вложением быстро становится громоздким.
Определение и вызов асинхронных функций
Асинхронная функция или асинхронный метод — это особый вид функции или метода, который может быть приостановлен во время выполнения. Это отличается от обычных синхронных функций и методов, которые либо выполняются до завершения, генерируют ошибку или никогда не возвращают значение. Асинхронная функция или метод всё ещё делает одно из этих трёх действий, но также может приостановиться посредине, когда ожидает чего-то. Внутри тела асинхронной функции или метода вы помечаете каждое из этих мест, где выполнение может быть приостановлено.
Чтобы указать, что функция или метод является асинхронным, вы записываете ключевое слово async в её объявлении после параметров, аналогично тому, как вы используете throws для помечания выбрасывающей функции. Если функция или метод возвращает значение, вы записываете async перед стрелкой возврата (->). Например, вот как вы можете получить имена фотографий в галерее:
func listPhotos(inGallery name: String) async -> [String] {
let result = // ... some asynchronous networking code ...
return result
} Для функции или метода, который является одновременно асинхронным и выбрасывающим, вы записываете async перед throws.
При вызове асинхронного метода выполнение приостанавливается до тех пор, пока этот метод не вернёт значение. Вы записываете await перед вызовом, чтобы отметить возможную точку приостановки. Это как написать try при вызове выбрасывающей функции, чтобы отметить возможные изменения в ходе программы в случае возникновения ошибки. Внутри асинхронного метода поток выполнения приостанавливается только при вызове другого асинхронного метода — приостановка никогда не является неявной или предварительной — что означает, что каждая возможная точка приостановки помечена await. Пометка всех возможных точек приостановки в вашем коде помогает сделать конкурентный код более понятным и читаемым.
Например, код ниже получает имена всех изображений в галерее, а затем отображает первое изображение:
let photoNames = await listPhotos(inGallery: "Summer Vacation") let sortedNames = photoNames.sorted() let name = sortedNames[0] let photo = await downloadPhoto(named: name) show(photo)
Поскольку функции listPhotos(inGallery:) и downloadPhoto(named:) обе должны выполнять запросы к сети, они могут занимать относительно много времени для завершения. Сделайте их асинхронными, записав async перед стрелкой возврата, что позволит остальной части приложения продолжать работу, пока этот код ждёт готовности изображения.
Для понимания конкурентной природы примера выше, вот один из возможных порядков выполнения:
Код начинает выполняться с первой строки и выполняется до первой
await. Он вызывает функциюlistPhotos(inGallery:)и приостанавливает выполнение, ожидая возврата этой функции.В то время как выполнение этого кода приостановлено, некоторый другой конкурентный код в той же программе выполняется. Например, возможно, длительная фоновая задача продолжает обновлять список новых фотогалерей. Этот код также выполняется до следующей точки приостановки, отмеченной
await, или до завершения.После возврата
listPhotos(inGallery:)этот код продолжает выполнение, начиная с этой точки. Он присваивает возвращённое значениеphotoNames.Строки, определяющие
sortedNamesиname, представляют собой обычный синхронный код. Поскольку ничего не помеченоawaitв этих строках, возможных точек приостановки нет.Следующая
awaitотмечает вызов функцииdownloadPhoto(named:). Этот код снова приостанавливает выполнение до возврата этой функции, предоставляя другим конкурентным кодам возможность выполнения.После возврата
downloadPhoto(named:)его возвращаемое значение присваиваетсяphoto, а затем передаётся в качестве аргумента при вызовеshow(_:).
Возможные точки приостановки в вашем коде, отмеченные await, указывают на то, что текущий фрагмент кода может приостановить выполнение, ожидая возврата асинхронной функции или метода. Это также называется уступкой потока, потому что в скрытом режиме Swift приостанавливает выполнение вашего кода в текущем потоке и запускает вместо него другой код в этом потоке. Так как код с await должен уметь приостанавливать выполнение, только определённые места в вашей программе могут вызывать асинхронные функции или методы:
Код в теле асинхронной функции, метода или свойства.
Код в статическом
main()методе структуры, класса или перечисления, помеченном@main.Код в неопределённой дочерней задаче, как показано в <doc:Concurrency#Unstructured-Concurrency> ниже.
Вы можете явно вставить точку приостановки, вызвав метод Task.yield().
func generateSlideshow(forGallery gallery: String) async {
let photos = await listPhotos(inGallery: gallery)
for photo in photos {
// ... render a few seconds of video for this photo ...
await Task.yield()
}
} Предполагая, что код, который отображает видео, является синхронным, он не содержит каких-либо точек приостановки. Работа по отображению видео также может занять много времени. Однако вы можете периодически вызывать Task.yield(), чтобы явно добавить точки приостановки. Структурирование длительного кода таким образом позволяет Swift находить баланс между выполнением этой задачи и выполнением других задач в вашей программе.
Метод Task.sleep(for:tolerance:clock:) полезен при написании простого кода, чтобы узнать, как работает конкурентность. Этот метод приостанавливает текущую задачу как минимум на указанное количество времени. Вот версия функции listPhotos(inGallery:), которая использует sleep(for:tolerance:clock:) для моделирования ожидания сетевой операции:
func listPhotos(inGallery name: String) async throws -> [String] {
try await Task.sleep(for: .seconds(2))
return ["IMG001", "IMG99", "IMG0404"]
} Версия listPhotos(inGallery:) в коде выше является и асинхронной, и выбрасывающей, потому что вызов Task.sleep(until:tolerance:clock:) может сгенерировать ошибку. При вызове этой версии listPhotos(inGallery:) вы пишете и try, и await:
let photos = try await listPhotos(inGallery: "A Rainy Weekend")
Асинхронные функции имеют некоторое сходство с выбрасывающими функциями: когда вы определяете асинхронную или выбрасывающую функцию, вы помечаете её async или throws, а вызовы этой функции помечаются await или try. Асинхронная функция может вызывать другую асинхронную функцию, точно так же, как выбрасывающая функция может вызывать другую выбрасывающую функцию.
Однако есть очень важное различие. Вы можете обернуть выбрасывающий код в блок do-catch, чтобы обработать ошибки, или использовать Result, чтобы сохранить ошибку для обработки кодом в другом месте. Эти подходы позволяют вызывать выбрасывающие функции из невыбрасывающего кода. Например:
func availableRainyWeekendPhotos() -> Result<[String], Error> {
return Result {
try listDownloadedPhotos(inGallery: "A Rainy Weekend")
}
} В отличие от этого, нет безопасного способа обернуть асинхронный код, чтобы вы могли вызывать его из синхронного кода и ждать результата. Стандартная библиотека Swift намеренно исключает эту небезопасную функциональность — попытка реализовать её самостоятельно может привести к проблемам, таким как скрытые гонки, проблемы с потоками и тупики. При добавлении конкурентного кода в существующий проект работайте сверху вниз. То есть начните с преобразования верхнего уровня кода для использования конкурентности, а затем начните преобразование функций и методов, которые он вызывает, продвигаясь по архитектуре проекта по одному слою за раз. Нет возможности применить подход снизу вверх, так как синхронный код никогда не может вызывать асинхронный код.
Асинхронные последовательности
Функция listPhotos(inGallery:) в предыдущем разделе асинхронно возвращает весь массив сразу, после того как все элементы массива будут готовы. Другой подход — ожидать один элемент коллекции за раз с помощью асинхронной последовательности. Вот как выглядит итерация по асинхронной последовательности:
import Foundation
let handle = FileHandle.standardInput
for try await line in handle.bytes.lines {
print(line)
} Вместо использования обычного цикла for-in, в примере выше записывается for с await после него. Как и при вызове асинхронной функции или метода, запись await указывает на возможную точку приостановки. Цикл for-await-in потенциально приостанавливает выполнение в начале каждой итерации, ожидая, когда следующий элемент станет доступным.
Точно так же, как вы можете использовать собственные типы в цикле for-in, добавив соответствие протоколу Sequence, вы можете использовать свои собственные типы в цикле for-await-in, добавив соответствие протоколу AsyncSequence.
Вызов асинхронных функций параллельно
Вызов асинхронной функции с await выполняет только один фрагмент кода за раз. Пока асинхронный код выполняется, вызывающая сторона ожидает завершения этого кода, прежде чем переходить к выполнению следующей строки кода. Например, чтобы получить первые три фотографии из галереи, вы можете дождаться трёх вызовов функции downloadPhoto(named:) следующим образом:
let firstPhoto = await downloadPhoto(named: photoNames[0]) let secondPhoto = await downloadPhoto(named: photoNames[1]) let thirdPhoto = await downloadPhoto(named: photoNames[2]) let photos = [firstPhoto, secondPhoto, thirdPhoto] show(photos)
Этот подход имеет существенный недостаток: хотя загрузка асинхронна и позволяет выполнять другие задачи во время процесса загрузки, только один вызов downloadPhoto(named:) выполняется за раз. Каждая фотография загружается полностью, прежде чем начнётся загрузка следующей. Однако нет необходимости в ожидании — каждая фотография может загружаться независимо или даже одновременно.
Чтобы вызывать асинхронную функцию и позволить ей выполняться параллельно с кодом вокруг неё, запишите async перед let при определении константы, а затем запишите await каждый раз, когда вы используете константу.
async let firstPhoto = downloadPhoto(named: photoNames[0]) async let secondPhoto = downloadPhoto(named: photoNames[1]) async let thirdPhoto = downloadPhoto(named: photoNames[2]) let photos = await [firstPhoto, secondPhoto, thirdPhoto] show(photos)
В этом примере все три вызова downloadPhoto(named:) начинаются без ожидания завершения предыдущего. Если доступно достаточно системных ресурсов, они могут выполняться одновременно. Ни один из этих вызовов функций не помечен с await, потому что код не приостанавливается для ожидания результата функции. Вместо этого выполнение продолжается до строки, где определена photos — в этот момент программе необходимы результаты этих асинхронных вызовов, поэтому вы пишите await, чтобы приостановить выполнение, пока все три фотографии не закончат загружаться.
Вот как можно понять различия между этими двумя подходами:
- Вызывайте асинхронные функции с
await, когда код в последующих строках зависит от результата этой функции. Это создаёт работу, выполняемую последовательно. - Вызывайте асинхронные функции с
async-let, когда вам не нужен результат до более позднего этапа вашего кода. Это создаёт работу, которая может выполняться параллельно. - И
await, иasync-letпозволяют запускать другой код во время приостановки. - В обоих случаях вы помечаете возможную точку приостановки с
await, чтобы указать, что выполнение будет приостановлено, если необходимо, до возвращения результата асинхронной функции.
Вы также можете смешивать оба эти подхода в одном коде.
Задачи и группы задач
Задача — это единица работы, которая может выполняться асинхронно как часть вашей программы. Весь асинхронный код выполняется в рамках некоторой задачи. Сама задача выполняет только одно действие за раз, но когда вы создаёте несколько задач, Swift может запланировать их выполнение одновременно.
Синтаксис async-let, описанный в предыдущем разделе, неявно создаёт дочернюю задачу — этот синтаксис хорошо работает, когда вы уже знаете, какие задачи вашей программе нужно выполнить. Вы также можете создать группу задач (экземпляр TaskGroup) и явно добавить дочерние задачи в эту группу, что даёт вам больший контроль над приоритетом и отменением, а также позволяет создавать динамическое количество задач.
Задачи организованы в иерархии. Каждая задача в данной группе задач имеет ту же родительскую задачу, и каждая задача может иметь дочерние задачи. Из-за явного отношения между задачами и группами задач этот подход называется структурированной конкурентностью. Явное родительско-дочернее отношение между задачами имеет несколько преимуществ:
В родительской задаче вы не можете забыть дождаться завершения её дочерних задач.
При установлении более высокого приоритета дочерней задачи приоритет родительской задачи автоматически повышается.
При отмене родительской задачи каждая из её дочерних задач также автоматически отменяется.
Локальные значения задачи эффективно и автоматически распространяются на дочерние задачи.
Вот ещё одна версия кода для загрузки фотографий, которая обрабатывает любое количество фотографий:
await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
for await photo in group {
show(photo)
}
} Код выше создаёт новую группу задач, а затем создаёт дочерние задачи для загрузки каждой фотографии в галерее. Swift запускает столько из этих задач одновременно, сколько позволяют условия. Как только дочерняя задача закончит загрузку фотографии, эта фотография отображается. Нет гарантии порядка завершения дочерних задач, поэтому фотографии из этой галереи могут отображаться в любом порядке.
Примечание: если код загрузки фотографии может вызывать ошибку, вы должны использовать
withThrowingTaskGroup(of:returning:body:)вместо него.
В приведённом коде каждая фотография загружается и отображается, поэтому группа задач не возвращает никаких результатов. Для группы задач, которая возвращает результат, вы добавляете код, который накапливает свой результат внутри замыкания, которое вы передаёте в withTaskGroup(of:returning:body:).
let photos = await withTaskGroup(of: Data.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
group.addTask {
return await downloadPhoto(named: name)
}
}
var results: [Data] = []
for await photo in group {
results.append(photo)
}
return results
} Как и в предыдущем примере, этот пример создаёт дочернюю задачу для каждой фотографии, чтобы загрузить её. В отличие от предыдущего примера, цикл for-await-in ожидает завершения следующей дочерней задачи, добавляет результат этой задачи в массив результатов, а затем продолжает ожидать, пока все дочерние задачи не завершатся. Наконец, группа задач возвращает массив загруженных фотографий как свой общий результат.
Отмена задач
Swift concurrency использует модель кооперативного отмены. Каждая задача проверяет, отменена ли она в соответствующие моменты своего выполнения и реагирует на отмену соответствующим образом. В зависимости от того, какую работу выполняет задача, ответ на отмену обычно означает одно из следующего:
- Вызов ошибки, подобной
CancellationError - Возврат
nilили пустого набора - Возврат частично завершённой работы
Загрузка изображений может занять много времени, если изображения большие или сеть медленная. Чтобы позволить пользователю остановить эту работу, не дожидаясь завершения всех задач, задачи должны проверять отмену и прекращать выполнение, если они отменены. Есть два способа, которыми задача может это сделать: вызвав метод типа Task.checkCancellation() или прочитав свойство типа Task.isCancelled. Вызов checkCancellation() вызывает ошибку, если задача отменена; бросающая задачу может распространить ошибку за пределы задачи, останавливая всю работу задачи. Это имеет преимущество простоты реализации и понимания. Для большей гибкости используйте свойство isCancelled, которое позволяет выполнять работы по очистке как часть остановки задачи, например, закрывать сетевые подключения и удалять временные файлы.
let photos = await withTaskGroup(of: Optional<Data>.self) { group in
let photoNames = await listPhotos(inGallery: "Summer Vacation")
for name in photoNames {
let added = group.addTaskUnlessCancelled {
guard !Task.isCancelled else { return nil }
return await downloadPhoto(named: name)
}
guard added else { break }
}
var results: [Data] = []
for await photo in group {
if let photo { results.append(photo) }
}
return results
} Код выше вносит несколько изменений по сравнению с предыдущей версией:
Каждая задача добавляется с помощью метода
TaskGroup.addTaskUnlessCancelled(priority:operation:), чтобы избежать запуска новой работы после отмены.После каждого вызова
addTaskUnlessCancelled(priority:operation:)код подтверждает, что новая дочерняя задача была добавлена. Если группа отменена, значениеaddedравноfalse— в этом случае код прекращает попытки загрузить дополнительные фотографии.Каждая задача проверяет отмену перед началом загрузки фотографии. Если она отменена, задача возвращает
nil.В конце группа задач пропускает значения
nilпри сборе результатов. Обработка отмены возвращениемnilозначает, что группа задач может вернуть частичный результат — фотографии, которые уже были загружены на момент отмены — вместо того, чтобы отбрасывать эту законченную работу.
Примечание: чтобы проверить, отменена ли задача извне этой задачи, используйте свойство экземпляра
Task.isCancelledвместо свойства типа.
Для работы, требующей немедленного уведомления об отмене, используйте метод Task.withTaskCancellationHandler(operation:onCancel:isolation:). Например:
let task = await Task.withTaskCancellationHandler {
// ...
} onCancel: {
print("Canceled!")
}
// ... some time later...
task.cancel() // Prints "Canceled!" При использовании обработчика отмены отмена задачи всё ещё кооперативна: задача либо выполняется до конца, либо проверяет отмену и останавливается раньше. Поскольку задача всё ещё работает, когда запускается обработчик отмены, избегайте совместного использования состояния между задачей и её обработчиком отмены, что может привести к гонке.
Неструктурированная конкурентность
В дополнение к структурированным подходам к конкурентности, описанным в предыдущих разделах, Swift также поддерживает неструктурированную конкурентность. В отличие от задач, которые являются частью группы задач, неструктурированная задача не имеет родительской задачи. У вас есть полная гибкость в управлении неструктурированными задачами, как требуется вашей программе, но вы также полностью несёте ответственность за их корректность. Чтобы создать неструктурированную задачу, выполняемую на текущем акторе, вызовите инициализатор Task.init(priority:operation:). Чтобы создать неструктурированную задачу, которая не является частью текущего актора, более точно известную как отделенная задача, вызовите метод класса Task.detached(priority:operation:). Оба эти действия возвращают задачу, с которой вы можете взаимодействовать — например, дождаться её результата или отменить её.
let newPhoto = // ... some photo data ...
let handle = Task {
return await add(newPhoto, toGalleryNamed: "Spring Adventures")
}
let result = await handle.value Дополнительную информацию о управлении отделёнными задачами см. в Task.
Актеры
Вы можете использовать задачи для разделения вашей программы на изолированные, конкурирующие части. Задачи изолированы друг от друга, что делает безопасным их одновременное выполнение, но иногда вам нужно обмениваться информацией между задачами. Акторы позволяют безопасно обмениваться информацией между конкурирующим кодом.
Как и классы, акторы являются типами ссылок, поэтому сравнение типов значений и ссылочных типов в <doc:ClassesAndStructures#Classes-Are-Reference-Types> относится как к акторам, так и к классам. В отличие от классов, акторы позволяют только одной задаче одновременно получать доступ к их изменяемому состоянию, что делает безопасным взаимодействие кода из нескольких задач с одним экземпляром актора. Например, вот актор, который записывает температуры:
actor TemperatureLogger {
let label: String
var measurements: [Int]
private(set) var max: Int
init(label: String, measurement: Int) {
self.label = label
self.measurements = [measurement]
self.max = measurement
}
} Вы объявляете актор с помощью ключевого слова actor, за которым следует его определение в паре фигурных скобок. Актор TemperatureLogger имеет свойства, к которым может получить доступ другой код вне актора, и ограничивает доступ к свойству max, так что только код внутри актора может обновлять максимальное значение.
Вы создаёте экземпляр актора, используя тот же синтаксис инициализации, что и для структур и классов. При доступе к свойству или методу актора вы используете await для обозначения потенциальной точки приостановки. Например:
let logger = TemperatureLogger(label: "Outdoors", measurement: 25) print(await logger.max) // Prints "25"
В этом примере доступ к logger.max является потенциальной точкой приостановки. Поскольку актор позволяет только одной задаче одновременно получать доступ к своему изменяемому состоянию, если код из другой задачи уже взаимодействует с логгером, этот код приостанавливается, ожидая доступа к свойству.
В отличие от этого, код, являющийся частью актора, не записывает await при доступе к свойствам актора. Например, вот метод, который обновляет TemperatureLogger с новой температурой:
extension TemperatureLogger {
func update(with measurement: Int) {
measurements.append(measurement)
if measurement > max {
max = measurement
}
}
} Метод update(with:) уже выполняется в акторе, поэтому он не помечает свой доступ к свойствам, таким как max, с помощью await. Этот метод также демонстрирует одну из причин, по которой акторы позволяют только одной задаче одновременно взаимодействовать с их изменяемым состоянием: некоторые обновления состояния актора временно нарушают инварианты. Актор TemperatureLogger отслеживает список температур и максимальную температуру, и обновляет максимальную температуру при записи нового измерения. В середине обновления, после добавления нового измерения, но до обновления max, логгер температур находится в временном несогласованном состоянии. Предотвращение одновременного взаимодействия нескольких задач с одним экземпляром предотвращает проблемы, подобные следующей последовательности событий:
- Ваш код вызывает метод
update(with:). Он сначала обновляет массивmeasurements. - Перед тем как ваш код может обновить
max, код в другом месте считывает максимальное значение и массив температур. - Ваш код завершает обновление, изменив
max.
В этом случае код, работающий в другом месте, получит некорректную информацию, потому что его доступ к актору был вставлен в середину вызова update(with:), в то время как данные были временно невалидными. Вы можете предотвратить эту проблему, используя акторы Swift, поскольку они позволяют выполнять только одну операцию над их состоянием за раз, и этот код может быть прерван только в тех местах, где await отмечает точку приостановки. Так как update(with:) не содержит точек приостановки, другой код не может получить доступ к данным в середине обновления.
Если код вне актора пытается получить прямой доступ к этим свойствам, как при доступе к свойствам структуры или класса, вы получите ошибку времени компиляции. Например:
print(logger.max) // Error
Доступ к logger.max без записи await терпит неудачу, потому что свойства актора являются частью изолированного локального состояния этого актора. Код для доступа к этому свойству должен выполняться как часть актора, что является асинхронной операцией и требует записи await. Swift гарантирует, что только код, выполняемый в акторе, может получить доступ к локальному состоянию этого актора. Это гарантия известна как изоляция актора.
Следующие аспекты модели параллельности Swift работают вместе, чтобы упростить работу с общим изменяемым состоянием:
Код между потенциальными точками приостановки выполняется последовательно, без возможности прерывания другим конкурирующим кодом.
Код, взаимодействующий с локальным состоянием актора, выполняется только в этом акторе.
Актор выполняет только один фрагмент кода за раз.
Благодаря этим гарантиям код, который не включает await и находится внутри актора, может выполнять обновления без риска, что другие части программы увидят временно невалидное состояние. Например, код ниже преобразует измеренные температуры из градусов Фаренгейта в Цельсия:
extension TemperatureLogger {
func convertFahrenheitToCelsius() {
for i in measurements.indices {
measurements[i] = (measurements[i] - 32) * 5 / 9
}
}
} Код выше преобразует массив измерений по одному. Пока операция map выполняется, некоторые температуры находятся в градусах Фаренгейта, а другие — в градусах Цельсия. Однако, поскольку ни один из кодов не включает await, в этом методе нет потенциальных точек приостановки. Состояние, которое изменяет этот метод, принадлежит актору, который защищает его от чтения или изменения другим кодом, кроме того случая, когда этот код выполняется в акторе. Это означает, что другой код не может прочитать список частично преобразованных температур во время преобразования единиц измерения.
В дополнение к написанию кода в акторе, который защищает временное невалидное состояние путём исключения потенциальных точек приостановки, вы можете переместить этот код в синхронный метод. Метод convertFahrenheitToCelsius() выше — синхронный метод, поэтому он гарантированно никогда не содержит потенциальных точек приостановки. Эта функция инкапсулирует код, временно делающий модель данных несогласованной, и упрощает понимание, что никакой другой код не может быть запущен до восстановления согласованности данных после завершения работы. В будущем, если вы попытаетесь добавить конкурирующий код в эту функцию, введя потенциальную точку приостановки, вместо введения ошибки вы получите ошибку времени компиляции.
Типы, совместимые с передачей
Задачи и акторы позволяют разделить программу на части, которые могут безопасно выполняться одновременно. Внутри задачи или экземпляра актора часть программы, содержащая изменяемое состояние, такие как переменные и свойства, называется областью параллельности. Некоторые виды данных не могут быть совместно использованы между областями параллельности, потому что эти данные содержат изменяемое состояние, но не защищены от перекрывающегося доступа.
Тип, который может быть совместно использован из одной области параллельности в другую, называется совместимым с передачей типом. Например, он может быть передан в качестве аргумента при вызове метода актора или возвращён в результате выполнения задачи. В примерах ранее в этой главе не обсуждалась совместимость с передачей, поскольку в этих примерах используются простые типы значений, которые всегда безопасны для обмена данными между областями параллельности. В отличие от этого, некоторые типы небезопасны для передачи через области параллельности. Например, класс, содержащий изменяемые свойства и не сериализующий доступ к этим свойствам, может генерировать непредсказуемые и неверные результаты, когда вы передаёте экземпляры этого класса между различными задачами.
Вы помечаете тип как совместимый с передачей, объявив соответствие протоколу Sendable. Этот протокол не имеет никаких требований к коду, но имеет семантические требования, которые Swift обеспечивает. В общем, существует три способа, которыми тип может быть совместимым с передачей:
- Тип является типом значения, а его изменяемое состояние состоит из других данных, совместимых с передачей — например, структура со свойствами хранения, которые совместимы с передачей, или перечисление со связанными значениями, которые совместимы с передачей.
- Тип не имеет никакого изменяемого состояния, а его неизменяемое состояние состоит из других данных, совместимых с передачей — например, структура или класс, которые имеют только свойства только для чтения.
- Тип имеет код, который обеспечивает безопасность его изменяемого состояния, например, класс, помеченный как
@MainActor, или класс, который сериализует доступ к своим свойствам на определённой нити или очереди.
Подробный список семантических требований см. в справке по протоколу Sendable.
Некоторые типы всегда совместимы с передачей, например, структуры, которые имеют только свойства, совместимые с передачей, и перечисления, которые имеют только связанные значения, совместимые с передачей. Например:
struct TemperatureReading: Sendable {
var measurement: Int
}
extension TemperatureLogger {
func addReading(from reading: TemperatureReading) {
measurements.append(reading.measurement)
}
}
let logger = TemperatureLogger(label: "Tea kettle", measurement: 85)
let reading = TemperatureReading(measurement: 45)
await logger.addReading(from: reading) Поскольку TemperatureReading — это структура, которая имеет только свойства, совместимые с передачей, и структура не помечена как public или @usableFromInline, она подразумеваемо совместима с передачей. Вот версия структуры, где соответствие протоколу Sendable подразумевается:
struct TemperatureReading {
var measurement: Int
} Чтобы явно отметить тип как несовместимый с передачей, переопределяя неявное соответствие протоколу Sendable, используйте расширение:
struct FileDescriptor {
let rawValue: CInt
}
@available(*, unavailable)
extension FileDescriptor: Sendable { } Код выше показывает часть обертки вокруг дескрипторов файлов POSIX. Несмотря на то, что интерфейс для дескрипторов файлов использует целые числа для идентификации и взаимодействия с открытыми файлами, а целые значения совместимы с передачей, дескриптор файла не является безопасным для передачи через области параллельности.
В коде выше FileDescriptor — это структура, которая удовлетворяет критериям неявной совместимости с передачей. Однако расширение делает невозможным её соответствие протоколу Sendable, предотвращая тем самым совместимость типа с передачей.
This source file is part of the Swift.org open source project
Copyright © 2014 - 2025 Apple Inc. and the Swift project authors
Licensed under Apache License v2.0 with Runtime Library Exception