Task or Detached Task or DispatchQueue

Hello,

I have such code, that is converting async/ await code to completion-based code (for backwards compatibility purposes).

final class Repository {
  func fetchData() async throws -> Data {
    // simulate network call
    try await Task.sleep(for: .seconds(1))
    return Data()
  }

  func fetchData(completion: @escaping (Result<Data, Error>) -> Void) {
    Task {
      let result: Result<Data, Error>
      do {
          result = try await .success(fetchData())
      } catch {
          result = .failure(error)
      }
      Task.detached { @MainActor in
        // completion needs to run on the main thread
        // completion needs to be destroyed on the main thread
        completion(result) 
      }
    }
  }
}

The completion needs to run & be destroyed on the main thread, so there are 3 different alternatives (at least?) to achieve this

  1. Task { @MainActor in
  2. Task.detached { @MainActor in
  3. DispatchQueue.main.async {

I don’t really know what are the differences between these options, in this context, and therefore which one to choose. They all seem to be doing the same thing, which is executing some block of code on the main thread. But in case there are any differences between the 3 approaches, I would like to choose the one that suits best my case.

There is also a fourth option, that works, but it introduces an unwanted side-effect.

Annotating the Task with @MainActor, i.e

Task { @MainActor in
  let result: Result<Data, Error>
  do {
      result = try await .success(fetchData())
  } catch {
      result = .failure(error)
  }
  completion(result) 
}

This option introduces a slight delay in the execution of the task, as the block needs to be enqueued on the main thread, and then once it starts executing, it will immediately leave the main thread, due to try await fetchData().

There is another option:

func fetchData(completion: @escaping @MainActor (Result<Data, Error>) -> Void)

I guess you call this from UI code that is isolated to main actor anyway, so you’ll get this for free.

That won’t work, unfortunately, because the completion won’t be destroyed on the main thread, but on the Task’s thread, which is something that I have to avoid.

if you have strict requirements regarding in which execution context a resource is disposed, it may be best to model that destruction explicitly, rather than relying upon implicit reference counting behavior and hoping the last strong reference goes away where you want it to. as a simple example, you could use an optional wrapper around the callback to give you more control over tear down – if you need something more elaborate, a separate type could be used to box the closure and expose a destruction mechanism. additionally, if you need to perform some work on the main actor, you can do so without creating a new unstructured task via the MainActor.run(...) method. perhaps worth experimenting with something along the lines of this:

// ignoring some of the swift-version 6 errors for the moment
  func fetchData(completion: @escaping @MainActor (Result<Data, Error>) -> Void) {
    var completion = Optional(completion)
    Task {
      let result: Result<Data, Error>
      do {
          result = try await .success(fetchData())
      } catch {
          result = .failure(error)
      }
      await MainActor.run {
        let callback = completion.take()!
        callback(result)
        // or model resource teardown explicitly in a custom type and perform it here
      }
    }
  }
4 Likes