将 DispatchQueue.global().async 转换为同步函数的 async/await

huangapple go评论65阅读模式
英文:

Converting DispatchQueue.global().async to async/await for synchronous function

问题

I'm converting my old project from DispatchQueue to async await, there's a time-consuming operation which I wrapped in the global dispatch queue:

DispatchQueue.global().async {
  self.processed = processData(input)
  DispatchQueue.main.async {
    render() // reload UI with self.processed
  }
}
  • processData() is a time-consuming synchronous operation.
  • render() updates UI with processed data and needs to be on the main thread.

It seems like the closest thing to a global queue is to use Task.detached, but when using it, I can't mutate main actor-isolated properties like self.processed.

Then I thought about doing this:

processData(input: Input) async -> Output {
  await withCheckedContinuation { continuation in
    DispatchQueue.global().async {
      let output = process(input)
      continuation.resume(returning: output)
    }
  }
}

let processed = await processData(input)
render()

But it feels like I'm using async/await just for the sake of it while still using DispatchQueue. Any thoughts? Thanks!

英文:

I'm converting my old project from DispatchQueue to async await, there's a time consuming operation which I wrapped in the global dispatch queue:

DispatchQueue.global().async {
  self.processed = processData(input)
  DispatchQueue.main.async {
    render() // reload UI with self.processed
  }
}
  • processData() is a time consuming synchronous operation
  • render() updates UI with processed data, and it need to be on main thread

Looks like the closest thing to global queue is to use Task.detached, but using it I can't mutate main actor-isolated properties e.g. self.processed.

Then I thought about doing this:

processData(input: Input) async -> Output {
  await withCheckedContinuation { continuation in
    DispatchQueue.global().async {
      let output = process(input)
      continuation.resume(returning: output)
    }
  }
}

let processed = await processData(input)
render()

But it feels like doing it for the sake for using async/await while still use DispatchQueue. Any thoughts? Thanks!

答案1

得分: 0

你可以像这样使用具有后台优先级的任务:

Task.detached(priority: .background) {
  let output = await processData(input: yourInput)
  await MainActor.run {
    render()
  }
}

func processData(input: Input) async -> Output {
    // 进行耗时处理
}
英文:

You can use Task with background priority likewise:

Task.detached(priority: .background) {
  let output = await processData(input:yourInput)
  await MainActor.run {
    render()
  }
}

func processData(input: Input) async -> Output {
    //do your expensive processing
}

答案2

得分: 0

Yes, you theoretically can use a detached task.

Just make sure that the method which has this code is isolated to the main actor, as well as the render method, but that the processData method is not. E.g., in a type that is, itself, is actor isolated then you just need to mark processData as nonisolated:

@MainActor
final class Foo: Sendable {
    var processed: Output?

    func processAndUpdateUI(for input: Input) async throws {
        processed = try await Task.detached {
            try self.processData(input)
        }.value

        render()
    }

    func render() {}

    nonisolated func processData(_ input: Input) throws -> Output {}
}

But I would explicitly advise against the MainActor.run {…} or Task { @MainActor in …} patterns. That is brittle. The burden for getting onto the right actor should not fall on the shoulders of the caller. Just make sure that the relevant methods and properties are actor-isolated, and the compiler will ensure that you call these methods correctly, that values crossing actor-isolation boundaries are Sendable, etc.

A note on this latter point. When you pass objects between threads, you have to let the compiler know that they are thread-safe, i.e., Sendable. See WWDC 2022 video Eliminate data races using Swift Concurrency. In Swift 5.x, it will not always warn you if your types are Sendable or not, so consider turning changing the “Strict Concurrency Checking“ build setting to “Complete”:

将 DispatchQueue.global().async 转换为同步函数的 async/await

The first time or two you deal with Sendable types, it can seem awfully confusing, but hopefully the above videos will be helpful. But once you master Sendable conformance, it will become second nature, and you’ll wonder why you suffered through all those thread-safety headaches of yesteryear.

The problem is that detached tasks are unstructured concurrency. I.e., if you cancel the parent task, it will not propagate the cancellation to the child tasks.

So, I might remain within structured concurrency. E.g., I might move processData into its own actor:

@MainActor
final class Bar: Sendable {
    private let processor = Processor()
    var processed: Output?

    func processAndUpdateUI(for input: Input) async throws {
        processed = try await processor.processData(input)
        render()
    }

    func render() {}
}

actor Processor {
    func processData(_ input: Input) throws -> Output {
        while  {
            try Task.checkCancellation() // periodically check to make sure this hasn’t been canceled

            
        }

        return output
    }
}
英文:

Yes, you theoretically can use a detached task.

Just make sure that the method which has this code is isolated to the main actor, as well as the render method, but that the processData method is not. E.g., in a type that is, itself, is actor isolated then you just need to mark processData as nonisolated:

@MainActor
final class Foo: Sendable {
    var processed: Output?

    func processAndUpdateUI(for input: Input) async throws {
        processed = try await Task.detached {
            try self.processData(input)
        }.value

        render()
    }

    func render() {}

    nonisolated func processData(_ input: Input) throws -> Output {}
}

But I would explicitly advise against the MainActor.run {…} or Task { @MainActor in …} patterns. That is brittle. The burden for getting onto the right actor should not fall on the shoulders of the caller. Just make sure that the relevant methods and properties are actor-isolated and the compiler will ensure that you call these methods correctly, that values crossing actor-isolation boundaries are Sendable, etc.

A note on this latter point. When you pass objects between threads, you have to let the compiler know that they are thread-safe, i.e., Sendable. See WWDC 2022 video Eliminate data races using Swift Concurrency. In Swift 5.x, it will not always warn you if your types are Sendable or not, so consider turning changing the “Strict Concurrency Checking“ build setting to “Complete”:

将 DispatchQueue.global().async 转换为同步函数的 async/await

The first time or two you deal with Sendable types, it can seem awfully confusing, but hopefully the above videos will be helpful. But once you master Sendable conformance, it will become second nature and you’ll wonder why you suffered through all those thread-safety headaches of yesteryear.


The problem is that detached tasks are unstructured concurrency. I.e., if you cancel the parent task, it will not propagate the cancelation to the child tasks.

So, I might remain within structured concurrency. E.g., I might move processData into its own actor:

@MainActor
final class Bar: Sendable {
    private let processor = Processor()
    var processed: Output?

    func processAndUpdateUI(for input: Input) async throws {
        processed = try await processor.processData(input)
        render()
    }

    func render() {}
}

actor Processor {
    func processData(_ input: Input) throws -> Output {
        while  {
            try Task.checkCancellation() // periodically check to make sure this hasn’t been canceled

            
        }

        return output
    }
}

huangapple
  • 本文由 发表于 2023年5月10日 14:43:53
  • 转载请务必保留本文链接:https://go.coder-hub.com/76215564.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定