Каковы преимущества использования async-await в фоновом потоке?

Я пытаюсь понять правильное применение async-await в Swift. Допустим, метод async, не-IO-метод, который не выполняет внешних вызовов, вызывается из фонового потока для выполнения своего процесса, например, какого-то тяжелого метода обработки изображений.

func processImage(image: UIImage) async -> UIImage {
    ///
}

Task {
   let result = await processImage(image: image)
}
  1. Что происходит, когда код приостановлен и ждет результата? Поскольку это не внешний вызов, процесс должен выполняться где-то внутри пула потоков. И поскольку это не выполняется в том самом потоке, из которого вызывается метод, он должен выполняться в другом потоке. Создана ли подзадача для выполнения процесса? Насколько я понимаю, Task — это единица параллелизма, а отдельная задача не содержит параллелизма (за исключением async let), поэтому меня это немного сбивает с толку. Является ли задача параллельной или не параллельной?

  2. Я понимаю, что если этот метод вызывается из основного потока, неблокирующий аспект метода async освобождает поток для запуска элементов пользовательского интерфейса, тем самым обеспечивая бесшовное визуальное взаимодействие. Но в чем преимущество вызова метода async из фонового потока? Я не имею в виду синтаксический сахар возможности возвращать результаты или выдавать ошибки. Есть ли какие-либо преимущества в неблокирующем аспекте по сравнению с использованием синхронного метода, если метод не является методом ввода-вывода, вызываемым из фона? Другими словами, что он не блокирует? Если это параллельный процесс, он использует больше ресурсов для эффективной обработки нескольких вещей, но я не уверен, насколько выгоден параллельный процесс в этом случае.

Я предлагаю вам Meet async await. async await имеет мало общего с тредами и все, что связано с актерами. Вы должны никогда не использовать потоки для описания чего-либо с помощью async await, но не должны самостоятельно помещать функцию async в фоновый режим. Вы можете detach из actor.

lorem ipsum 05.02.2023 18:14

Вы имеете в виду презентацию WWDC? Спасибо тебе за это. Я взгляну.

Kevvv 05.02.2023 18:18

Да, это очень информативное видео, у них есть еще пара, связанных с этой темой, но это очень хорошее вступление.

lorem ipsum 05.02.2023 18:19

Еще одно замечание: все старые Queue, такие как DispatchQueue, не совместимы с async await, вы никогда не должны их смешивать, это одно или другое.

lorem ipsum 05.02.2023 18:22

IMO для «тяжелой обработки изображений» вы должны сосредоточиться на том, как это делается, то есть с использованием ЦП, нескольких ЦП, ГП или всего вышеперечисленного. Хотя способ доставки результата (асинхронный/ожидающий, обратный вызов и т. д.) действительно зависит от удобства для конкретного приложения (т. е. от того, какой поток пользовательского интерфейса связан, что еще делает приложение и т. д.)

rapiddevice 05.02.2023 20:31
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
5
96
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вам нужно перестать думать в терминах тредов, если вы хотите использовать 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 на самом деле является последовательным, но просто неблокирующим, поскольку он ожидает появления результатов до того, как произойдет следующий код.

Kevvv 05.02.2023 23:32

Его часто упускают из виду, хотя оно прямо указано в названии: Async-await — это «параллельная» система. Это позволяет нам писать простые линейные последовательности кода (без сложных обратных вызовов и замыканий), в то время как ЦП переключается между несколькими логическими задачами для достижения параллелизма. Когда мы нажимаем await, он приостанавливает этот конкретный путь выполнения, позволяя ЦП переключиться на какую-то другую задачу, всегда продвигаясь вперед, никогда не блокируя поток. Теперь он тоже может добиться параллелизма (с отдельными акторами, отдельными задачами, группами задач и т. д.), но в первую очередь это система параллелизма.

Rob 05.02.2023 23:58

Другие вопросы по теме