I have a function that performs a task which can either be performed immediately and without any blocking or it can only be performed after some delay. The caller of that function shouldn't need to care, that's why this function has a completion handler and will perform the task asynchronously if required and then call the completion handler once the task completed.
The current pseudo implementation is like this:
func doSomething ( arg, completion )
{
if (doSomethingInternal(arg)) {
completion()
} else {
scheduleForLater(arg, completion)
}
}
If doSomethingInternal()
returns true
, the task was performed, otherwise it wasn't performed and we schedule it for later.
Now here's the catch:
As you can see, if the task can be performed at once, the completion handler is called on the same thread that called doSomething()
and it is called even before doSomething()
has returned. In case it must be scheduled for later, the completion handler is called on a different thread and it may be called on that thread even before doSomething()
has returned or after it has returned (the later one is much more likely).
I considered this kind of implementation to be a good idea as it avoids an unnecessary thread switch and thus has a low overhead as long as the task can be performed instantly which is the case > 90% of the time. Yet when putting it to use, I ran into a problem, as demonstrated by the following (very simplified) pseudo code:
func performNextTask ( )
{
arg = getNextArg()
if (arg) doSomething(arg, { performNextTask() })
}
As getNextArg()
is thread-safe, it won't matter on what thread performNextTask()
is getting called and if I force doSomething()
to always be asynchronous, the entire code works correctly. However, if it performs synchronously all the time, the code above will crash as it runs into a stack overflow! That's because in that case you get a recursive call chain if there's always a next arg available.
So here's my design question: How do you usually deal with that situation?
Never be synchronous? Will work but result in sub-optimal performance because of unnecessary thread switches.
Let the completion know, if it is getting called synchronously or not? That way the completion itself can somehow deal with that situation, yet I have never seen any code doing that.
Not call the completion in the synchronous case and let the caller know that the completion won't be called as the task was already performed? I have seen that in the wild but to me that is code smell as many programmers will expect a completion to always be called when something has completed.
Any other ideas?