How to Handle Asynchronous Operations in BGContinuedProcessingTask

I would like to know whether BGContinuedProcessingTaskRequest supports executing asynchronous tasks internally, or if it can only execute synchronous tasks within BGContinuedProcessingTaskRequest?

Our project is very complex, and we now need to use BGContinuedProcessingTaskRequest to perform some long-running operations when the app enters the background (such as video encoding/decoding & export). However, our export interface is an asynchronous function, for example video.export(callback: FinishCallback). This export call returns immediately, and when the export completes internally, it calls back through the passed-in callback. So when I call BGTaskScheduler.shared.register to register a BGContinuedProcessingTask, what should be the correct approach? Should I directly call video.export(nil) without any waiting, or should I wait for the export function to complete in the callback? For example:

    BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.xxx.xxx.xxx.xxx", using: nil) { task in
                guard let continuedTask = task as? BGContinuedProcessingTask else {
                    task.setTaskCompleted(success: false)
                    return
                }
                
                let scanner = SmartAssetsManager.shared
                let semaphore = DispatchSemaphore(value: 0)

                continuedTask.expirationHandler = {
                    logError(items: "xwxdebug finished.")
                    semaphore.signal()
                }

                logInfo(items: "xwxdebug start!")

                video.export { _ in
                    semaphore.signal()
                }
                
                
                semaphore.wait()
                logError(items: "xwxdebug finished!")
            }

I would like to know whether BGContinuedProcessingTaskRequest supports executing asynchronous tasks internally.

Yes, this is 100% supported. FYI, it uses the same approach as the other task types, so everything I'm describing equally applies to the other task types in the background task framework.

or if it can only execute synchronous tasks within BGContinuedProcessingTaskRequest?

No, not at all. You CAN choose to do your work inside the task block, but you are under no obligation to do so and, in fact, properly implemented solutions for work like you're describing:

...(such as video encoding/decoding & export). However, our export interface is an asynchronous function, for example, video.export(callback: FinishCallback).

...probably SHOULDN'T try and "force" their work into the task block model. Doing so would mean either compromising performance by constraining yourself to a single thread or something like a semaphore to artificially block the task block, both of which are not ideal. And, yes, the code snippet you include is exactly what I would NOT do.

Swift async can potentially avoid/mitigate these issues; however, the point of this API is to perform "extended work“ - that means "minutes", not "seconds". I have a difficult time thinking of work that runs for minutes at a time and is best implemented inside a single block callback.

So when I call BGTaskScheduler.shared.register to register a BGContinuedProcessingTask, what should be the correct approach?

In terms of the API, all that's required is that you provide regular progress and that you complete the task when you finish the work or the task expires. Whether or not you block or return in the launch handler block doesn't matter to the API.

Should I directly call video.export(nil) without any waiting, or should I wait for the export function to complete in the callback?

So, there's actually an inherent contradiction to the API that makes the answer to this somewhat complicated. I have a forum post here that goes into this in more detail, but quoting myself from the end of that post:

"...if an app has an existing complex data processing pipeline that already works but "should" now continue into the background, then the right answer probably ISN'T "rework my entire data flow so it fits into BGContinuedProcessing". That's a great way to waste a lot of time introducing new bugs into working code. The simpler answer will often be to "attach" the BGContinuedProcessingTask to that existing pipeline without actually changing your existing code in any large-scale way. If the BGContinuedProcessing fails to start when you "expect” it, then you continue with the work anyway, which is exactly what you were doing before iOS 26."

Putting that in concrete terms, if an app has a button labeled "Export Critical Document Immediately..." and the user presses that button, the LAST thing your app should do is defer/delay that work because BGContinuedProcessingTask hasn't fired. The user just told your app to do something, so your app’s job is to "do it".

On the other hand, what you actually described here:

...to perform some long-running operations when the app enters the background (such as video encoding/decoding & export).

Isn't necessarily an appropriate use of the API. The point of this API is to finish the work the user wanted your app to do "now", NOT perform "background work" your app was otherwise deferring. We have other APIs for handling low-priority work without disrupting the user. This API is for finishing high-priority work, which means work you can't defer or cancel.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for your reply! When a user clicks "Export Video" and I immediately start the export (taking ~5 minutes for long videos), can I use BGContinuedProcessingTask only to update progress, rather than triggering the export itself within BGContinuedProcessingTask? I'm unclear if iOS only protects tasks triggered inside BGContinuedProcessingTask or the entire app process.

Thanks for your reply! When a user clicks "Export Video" and I immediately start the export (taking ~5 minutes for long videos), can I use BGContinuedProcessingTask only to update progress, rather than triggering the export itself within BGContinuedProcessingTask?

Yes. All the launch handler really HAS to do is export the new BGContinuedProcessingTask object so that the "rest" of your app can use it for things like updating progress and/or ending the task. That doesn't need to be any more complicated than using Dispatch to send it to some "other" part of your app.

That leads to here:

I'm unclear if iOS only protects tasks triggered inside BGContinuedProcessingTask or the entire app process.

First, as a broad background, it's worth understanding that many of the new technologies (like GCD blocks or Swift Tasks) are largely syntax "sugar" as far as the large system is concerned. As far as the "broader" system is concerned, there are only really two "basic" constructs:

  1. "Threads", which are individual execution streams that the scheduler provides CPU time.

  2. "Processes", which are collections of "related" threads.

Those two constructs are ALL that the larger system can really "see". At this point, we have a LONG list of different thread APIs*... all of which are "just threads" as far as the kernel is concerned.

*Off the top of my head... mach, pthread, NSThread, Dispatch, NSOperation, Swift Task... and those are just the ones that still work and that I happened to remember.

Returning to the question here:

I'm unclear if iOS only protects tasks triggered inside BGContinuedProcessingTask or the entire app process.

In general, the system’s basic "unit" of management is the process. So, for example, we wake and suspend entire processes, NOT threads. The issue here is that there isn't really any way for the system to safely interact with your app in terms of individual threads. For example, we don't know what your app is going to "do", so only allowing one thread to run means you'll promptly deadlock if you happen to call into another thread or even do "something" that relies on a lock that could be held by another thread. Note that the second issue is particularly critical, given that both Swift and Objective-C have locks embedded in their core runtime.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

How to Handle Asynchronous Operations in BGContinuedProcessingTask
 
 
Q