Я пытаюсь понять правильное применение async-await
в Swift. Допустим, метод async
, не-IO-метод, который не выполняет внешних вызовов, вызывается из фонового потока для выполнения своего процесса, например, какого-то тяжелого метода обработки изображений.
func processImage(image: UIImage) async -> UIImage {
///
}
Task {
let result = await processImage(image: image)
}
Что происходит, когда код приостановлен и ждет результата? Поскольку это не внешний вызов, процесс должен выполняться где-то внутри пула потоков. И поскольку это не выполняется в том самом потоке, из которого вызывается метод, он должен выполняться в другом потоке. Создана ли подзадача для выполнения процесса? Насколько я понимаю, Task
— это единица параллелизма, а отдельная задача не содержит параллелизма (за исключением async let
), поэтому меня это немного сбивает с толку. Является ли задача параллельной или не параллельной?
Я понимаю, что если этот метод вызывается из основного потока, неблокирующий аспект метода async
освобождает поток для запуска элементов пользовательского интерфейса, тем самым обеспечивая бесшовное визуальное взаимодействие. Но в чем преимущество вызова метода async
из фонового потока? Я не имею в виду синтаксический сахар возможности возвращать результаты или выдавать ошибки. Есть ли какие-либо преимущества в неблокирующем аспекте по сравнению с использованием синхронного метода, если метод не является методом ввода-вывода, вызываемым из фона? Другими словами, что он не блокирует? Если это параллельный процесс, он использует больше ресурсов для эффективной обработки нескольких вещей, но я не уверен, насколько выгоден параллельный процесс в этом случае.
Вы имеете в виду презентацию WWDC? Спасибо тебе за это. Я взгляну.
Да, это очень информативное видео, у них есть еще пара, связанных с этой темой, но это очень хорошее вступление.
Еще одно замечание: все старые Queue
, такие как DispatchQueue
, не совместимы с async await
, вы никогда не должны их смешивать, это одно или другое.
IMO для «тяжелой обработки изображений» вы должны сосредоточиться на том, как это делается, то есть с использованием ЦП, нескольких ЦП, ГП или всего вышеперечисленного. Хотя способ доставки результата (асинхронный/ожидающий, обратный вызов и т. д.) действительно зависит от удобства для конкретного приложения (т. е. от того, какой поток пользовательского интерфейса связан, что еще делает приложение и т. д.)
Вам нужно перестать думать в терминах тредов, если вы хотите использовать async/await
. В какой-то степени и по понятным причинам полезно продолжать использовать такие фразы, как «в основном потоке» и «в фоновом потоке», но это почти метафоры.
Вам просто нужно принять это, независимо от того, в каком «потоке» что-то работает, await
обладает волшебной силой сказать «задержи мое место» и позволить компьютеру уйти и заняться чем-то совершенно другим, пока не произойдет то, чего мы ждем. возвращается к нам. Ничего не блокирует, ничего не крутится. Это, действительно, большая часть смысла async/await
.
(Если вы хотите понять, как это работает под капотом, вам нужно выяснить, что такое «продолжение». Но в целом об этом действительно не стоит беспокоиться; это просто вопрос приведения в порядок вашей внутренней системы убеждений.)
Однако общее преимущество async/await
синтаксическое, а не механическое. Возможно, вы могли бы сделать все то же, что и через async/await
, используя какой-то другой механизм (Combine, DispatchQueue, Operation и т. д.). Но опыт показал, что, особенно в случае с DispatchQueue, новичкам (и не очень новичкам) очень трудно рассуждать о порядке выполнения строк кода, когда код асинхронный. С async/await
эта проблема исчезает: код выполняется в том порядке, в котором он появляется, как если бы он вообще не был асинхронным.
И не только программисты; компилятор не может рассуждать о правильности вашего кода DispatchQueue. Это не может помочь поймать вас на ваших ошибках (и я уверен, что вы сделали несколько в свое время; я, конечно, сделал). Но async/await
не такой; как раз наоборот: компилятор может рассуждать о вашем коде и может помочь сделать все аккуратно, безопасно и правильно.
Что касается фактического примера, который вы представляете, правильная реализация состоит в том, чтобы определить актера, чья работа заключается в выполнении трудоемкой задачи. Это по определению не будет главным действующим лицом; поскольку вы определили его, это будет то, что мы можем назвать фоновым актером; его методы будут вызываться из основного потока автоматически, а все остальное последует благодаря великолепию компилятора.
Вот пример (из моей книги), выполняющий именно то, о чем вы спрашиваете, — трудоемкий расчет. Это представление, которое, когда вы вызываете его общедоступный drawThatPuppy
метод, вычисляет грубое изображение множества Мандельброта из основного потока, а затем изображает это изображение внутри себя. Для ваших целей важно отметить, что в строках
self.bitmapContext = await self.calc.drawThatPuppy(center: center, bounds: bounds)
self.setNeedsDisplay()
фразы self.bitmapContext =
и self.setNeedsDisplay
выполняются в основном потоке, но вызов self.calc.drawThatPuppy
выполняется в фоновом потоке, потому что calc
является актором. Тем не менее, основной поток не блокируется во время выполнения self.calc.drawThatPuppy
; напротив, в это время может выполняться код другого основного потока. Это чудо!
// Mandelbrot drawing code based on https://github.com/ddeville/Mandelbrot-set-on-iPhone
import UIKit
extension CGRect {
init(_ x:CGFloat, _ y:CGFloat, _ w:CGFloat, _ h:CGFloat) {
self.init(x:x, y:y, width:w, height:h)
}
}
/// View that displays mandelbrot set
class MyMandelbrotView : UIView {
var bitmapContext: CGContext!
var odd = false
// the actor declaration puts us on the background thread
private actor MyMandelbrotCalculator {
private let MANDELBROT_STEPS = 200
func drawThatPuppy(center:CGPoint, bounds:CGRect) -> CGContext {
let bitmap = self.makeBitmapContext(size: bounds.size)
self.draw(center: center, bounds: bounds, zoom: 1, context: bitmap)
return bitmap
}
private func makeBitmapContext(size:CGSize) -> CGContext {
var bitmapBytesPerRow = Int(size.width * 4)
bitmapBytesPerRow += (16 - (bitmapBytesPerRow % 16)) % 16
let colorSpace = CGColorSpaceCreateDeviceRGB()
let prem = CGImageAlphaInfo.premultipliedLast.rawValue
let context = CGContext(data: nil, width: Int(size.width), height: Int(size.height), bitsPerComponent: 8, bytesPerRow: bitmapBytesPerRow, space: colorSpace, bitmapInfo: prem)
return context!
}
private func draw(center:CGPoint, bounds:CGRect, zoom:CGFloat, context:CGContext) {
func isInMandelbrotSet(_ re:Float, _ im:Float) -> Bool {
var fl = true
var (x, y, nx, ny) : (Float, Float, Float, Float) = (0,0,0,0)
for _ in 0 ..< MANDELBROT_STEPS {
nx = x*x - y*y + re
ny = 2*x*y + im
if nx*nx + ny*ny > 4 {
fl = false
break
}
x = nx
y = ny
}
return fl
}
context.setAllowsAntialiasing(false)
context.setFillColor(red: 0, green: 0, blue: 0, alpha: 1)
var re : CGFloat
var im : CGFloat
let maxi = Int(bounds.size.width)
let maxj = Int(bounds.size.height)
for i in 0 ..< maxi {
for j in 0 ..< maxj {
re = (CGFloat(i) - 1.33 * center.x) / 160
im = (CGFloat(j) - 1.0 * center.y) / 160
re /= zoom
im /= zoom
if (isInMandelbrotSet(Float(re), Float(im))) {
context.fill (CGRect(CGFloat(i), CGFloat(j), 1.0, 1.0))
}
}
}
}
}
private let calc = MyMandelbrotCalculator()
// jumping-off point: draw the Mandelbrot set
func drawThatPuppy() async {
let bounds = self.bounds
let center = CGPoint(x: bounds.midX, y: bounds.midY)
self.bitmapContext =
await self.calc.drawThatPuppy(center: center, bounds: bounds)
self.setNeedsDisplay()
}
// turn pixels of self.bitmapContext into CGImage, draw into ourselves
override func draw(_ rect: CGRect) {
if self.bitmapContext != nil {
let context = UIGraphicsGetCurrentContext()!
context.setFillColor(self.odd ? UIColor.red.cgColor : UIColor.green.cgColor)
self.odd.toggle()
context.fill(self.bounds)
let im = self.bitmapContext.makeImage()
context.draw(im!, in: self.bounds)
}
}
}
Язык программирования Swift: Concurrency определяет асинхронную функцию как «особый вид функции или метода, который можно приостановить, пока он находится на полпути к выполнению».
Итак, это обозначение async
на функции предназначено для действительно асинхронных подпрограмм, где функция приостанавливает/ожидает выполнения, пока выполняется асинхронный процесс. Типичным примером этого является получение данных с помощью URLSession
.
Но эта ресурсоемкая обработка изображений не является асинхронной задачей. Он синхронен по своей сути. Так что нет смысла отмечать это как async
. Кроме того, Task {…}
, вероятно, также не является правильным шаблоном, поскольку что создает «новую задачу верхнего уровня от имени текущего актера». Но вы, вероятно, не хотите, чтобы этот медленный синхронный процесс выполнялся на текущем актере (конечно, если это главный актер). Вам может понадобиться отдельная задача. Или поставить его на собственного актера.
В приведенном ниже фрагменте кода показано, как действительно асинхронные методы (например, сетевой запрос на получение данных, fetchImage
) отличаются от медленных синхронных методов (обработка изображения в processImage
):
func processedImage(from url: URL) async throws -> UIImage {
// fetch from network (calling `async` function)
let image = try await fetchImage(from: url)
// process synchronously, but do so off the current actor, so
// we don’t block this actor
return try await Task.detached {
await self.processImage(image)
}.value
}
// asynchronous method to fetch image
func fetchImage(from url: URL) async throws -> UIImage {
let (data, response) = try await URLSession.shared.data(from: url)
guard let image = UIImage(data: data) else { throw ImageError.notImage }
return image
}
// slow, synchronous method to process image
func processImage(_ image: UIImage) -> UIImage {
…
}
enum ImageError: Error {
case notImage
}
Для получения дополнительной информации см. видео WWDC 2021 Знакомство с async/await в Swift . Чтобы понять, что на самом деле означает await
(то есть точка приостановки) в рамках более широкой модели многопоточности, Параллелизм Swift: за кулисами может быть интересным просмотром.
Спасибо за ваш ответ. Это очень информативно. Это почти похоже на то, что async-await
на самом деле является последовательным, но просто неблокирующим, поскольку он ожидает появления результатов до того, как произойдет следующий код.
Его часто упускают из виду, хотя оно прямо указано в названии: Async-await — это «параллельная» система. Это позволяет нам писать простые линейные последовательности кода (без сложных обратных вызовов и замыканий), в то время как ЦП переключается между несколькими логическими задачами для достижения параллелизма. Когда мы нажимаем await
, он приостанавливает этот конкретный путь выполнения, позволяя ЦП переключиться на какую-то другую задачу, всегда продвигаясь вперед, никогда не блокируя поток. Теперь он тоже может добиться параллелизма (с отдельными акторами, отдельными задачами, группами задач и т. д.), но в первую очередь это система параллелизма.
Я предлагаю вам
Meet async await
.async await
имеет мало общего с тредами и все, что связано с актерами. Вы должны никогда не использовать потоки для описания чего-либо с помощьюasync await
, но не должны самостоятельно помещать функциюasync
в фоновый режим. Вы можетеdetach
изactor
.