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
@concurrentwrapper - Run it inside a
Task.detachedor anasync 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)
}
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.
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 ![]()
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")
}
}
but with this implementation the `continuation` isn’t protected
The Continuation is sendable.
my bad forgot this fact ![]()
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