I also did something similar to this recently and I can tell you it was non-trivial and I still need to do some checking on it. The idea was simple enough though, you have a function that reads an input stream on a background thread and then calls a completion handler when it’s done.
void readInputStreamAsync (std::unique_ptr<InputStream> inputStream,
std::function<void (MemoryBlock&)> completionCallback,
std::function<bool (float)> progressCallback = nullptr);
Under the hood however it loosely does the following:
• Uses std::async to create a std::future which calls a lambda
• The lambda captures the inputStream, completionCallback and progressCallback
• In the body of the lambda, the stream is read from in blocks
• Each block, the progressCallback is called
• When the stream is exhausted it calls the completionCallback with the result
• You can return false from the progress callback if you want to abort the function/thread early
However, you need to stash the std::future somewhere or it will just block until the function has returned making it synchronous and effectively useless. For this, I have a hidden/internal singleton object with a std::vector<std::future<void>> which holds the futures.
Great, but now you need a way to clear up the futures once they’ve completed. For this, the singleton is also an AsyncUpdater and the function bodies call triggerAsyncUpdate on it once they’ve finished their work (you have to make sure this also gets called if they exit early so I use a kind of ScopeGuard/gsl:: final_action to ensure this). In the handleAsyncUpdate method I then iterate the vector, check to see if each future’s state is invalid (to avoid it throwing exceptions) and then call wait_for (...0) to check the status. If it’s ready, the future is done with and it gets removed from the vector.
Now what happens if the app tries to exit before the threads have finished? In this case, we need the futures to finish running to avoid unfinished writes etc. To achieve this, the singleton also inherits from DeletedAtShutdown. When the singleton is deleted we simply need to clear the futures vector. All the futures will block until completed.
This all sounds good so far except that it’s not very usable. In reality (and in my use case these are web streams), the UI that requires these web streams are read may be closed or disappear before the stream completes. We don’t want to leave loads of these futures running for no reason.
To handle this case, we use the progressCallback supplied to the original function call. This can be used to signal early exit by returning false and the stream read operation will be abandoned. If we capture a weak reference to the calling Component, then if that becomes nullptr, we know the UI won’t be needing the function anymore and we can signal early exit. Something as simple as:
[sp = SafePointer<Component> (this)] (float) { return sp != nullptr; }
Of course, we also need to be sure that whatever we capture in the std::function completionCallback argument also only uses valid objects. This can often involve more SafePointer captures, checks and MessageManager::callAsync calls to make sure everything happens on the correct thread.
As I said, I’m not 100% finished with this yet, there may be more edge cases to handle. If you’re only calling a single function the original call will be a bit simpler but you’ll probably still have to deal with all the safe captures to avoid calling dangling objects.
If in your app you’ve already got a juce::ThreadPool up and running, creating a ThreadPoolJob subclass that takes a lambda is pretty trivial.