Running func on Background

What is the best way to run a function with is non isolated to the background thread without making it async, in other words I don’t want it to run on the callers actor because the caller is MainActor most of the time but this logic is background related to analytics. Approachable concurrency is enabled

without making it async

Do you need to await its result? If yes, you have two options:

  • Call it from an async @concurrent wrapper
  • Run it inside a Task.detached or an async let

You'd still require asynchrony somewhere because by definition you're trying to hop off the current actor, which requires the calling function to suspend.

But since you're saying that it's related to analytics, and such functions are typically fire-and-forget "just record this event" kind of invocations where you do not need to await the result, your AnalyticsHandler or whatever it's called should instead offer a mutex-protected queue into which you synchronously submit events to process. This also ensures correct ordering (since tasks are not guaranteed to be scheduled in the order of submission). These events could be either plain structs or @Sendable closures.

You can create an AsyncStream inside the analytics handler upon initialization, store its continuation in the handler, and clients would call something like this:

func record(_ event: AnalyticsEvent) { // note: synchronous!
    eventsStreamContinuation.yield(event)
}
3 Likes

what about creating something like that

    public func identify(event: IdentifyEvent) {
        Task {
            await self.identify(event: event)
        }
    }

    @concurrent private func identify(event: IdentifyEvent) async {
        
    }

This works, but is somewhat less optimal because by the point you're creating a task, you can just choose to make it a detached one to begin with:

public func identify(event: IdentifyEvent) {
    Task.detached {
        // just call the synchronous version here,
        // no need for a `@concurrent` wrapper
        self.identifySynchronous(event: event)
    }
}

Again, keep in mind that this can destroy the ordering of events though.

2 Likes

Because we can I have more than one Task at the same time right and there is no way to know which one will finish first.

I think I will have to use the AsyncStrem solution but can you explain it more please

private let analyticsQueue = DispatchQueue(
    label: "",
    qos: .utility
)

I think I will end up using serial dispatchqeue :sweat_smile:

You can try to wrap analytics client into actor with some stream and nonisolated stuff and should work, something like:

actor AnalyticsClient {
  
  private let messageStream: AsyncStream<String>
  private nonisolated let continuation: AsyncStream<String>.Continuation
  private let client: Client
  
  init(client: Client) {
    self.client = client
    (self.messageStream, self.continuation) = AsyncStream<String>.makeStream()
    Task {
      await start()
    }
  }
  
  func start() async {
    for await message in messageStream {
      print(message)
      await send(message: message)
    }
  }
  
  nonisolated func handle(message: String) {
    self.continuation.yield(message)
  }
  
  nonisolated func stop() {
    self.continuation.finish()
  }
  
  private func send(message: String) async {
    do {
      _ = try await client.send(message)
    } catch {
      // handle/log error as needed
      print("just a demo")
    }
  }
  
  deinit {
    print("deinit")
  }
}
2 Likes

but with this implementation the `continuation` isn’t protected

The Continuation is sendable.

my bad forgot this fact :sweat_smile:

To be honest I decided to go with the straight forward implementation using GCD:

private let analyticsQueue = DispatchQueue(label: "com.analyticsQueue",qos: .utility)

   public func track(event: AnalyticsEvent) {
        analyticsQueue.async { [weak self] in
            guard let self else { return }
        }
    }

Don't you need to do a network request inside queue?

nop