I have a situation where I need to compute something at-most-once. This value is computed asynchronously but also needs to be available without await-ing. If the value isn't computed, then nil is fine. Basically, the following protocol:
/// A value that is computed at most once. The value is asynchronously
/// computed the first time it's accessed.
public protocol AsyncLazy<T> where T : Sendable {
associatedtype T
/// The value, or nil if it hasn't been fetched yet.
var valueOrNil: T? { get }
/// The value, once it is computed
var value: T { get async }
}
It would be used like this:
val someValue = AsyncLazyImpl { someComputation() }
...
// On main thread...
let x = someValue.valueOrNil
// On other threads...
let x = await someValue.value
How would I implement this? I'm lost trying to make sense of what locks and concurrency primitives I'd use.
I gather that
someComputationis slow, synchronous, and does not explicitlyyieldduring the calculation. If so, that means we have to get it out of the Swift concurrency system. In WWDC 2022 video Visualize and optimize Swift concurrency, Apple says:So, with the above caveat in mind, you theoretically could do something like:
Generally, combining GCD with Swift concurrency is a mistake, but given the constraints of the original question, this is an exception to the rule.
Please note that this is not a generalized solution, but a straw-man that is narrowly answering your question. A few observations:
I have isolated this whole thing to the main actor so that you can fetch the
currentValue(what you calledvalueOrNil) from there without awaiting. (If you were only invoking this from asynchronous Swift concurrency contexts, you could instead just make this anactor.)I have supplied an
asynchronousValuefunction (which is analogous to thevalueproperty in your example) that awaits theTaskthat was initiated when theAsyncValuewas instantiated.Be forewarned that this just launches a task on a global queue (which is subject to the thread-explosion risks that plague legacy GCD code, namely, if you created lots of these objects). You should consider using a static serial queue, or operation queue with some reasonable
maxConcurrentOperationCountor the like.Also note that this is non-cancellable.
Having illustrated a possible solution, I would consider this an anti-pattern, because we generally strive to:
I would encourage you to reevaluate the situation in which you contemplated using this pattern, and see if there might be some more idiomatic Swift concurrency approach.