Processes & Concurrency

RSS for tag

Discover how the operating system manages multiple applications and processes simultaneously, ensuring smooth multitasking performance.

Concurrency Documentation

Posts under Processes & Concurrency subtopic

Post

Replies

Boosts

Views

Activity

Background Task Scheduler
Hello, An application I am working on would like to schedule push notifications for a medication reminder app. I am trying to use BGTaskScheduler to wake up periodically and submit the notifications based on the user's medication schedule. I set up the task registration in my AppDelegate's didFinishLaunchingWithOptions method: BGTaskScheduler.shared.register( forTaskWithIdentifier: backgroundTaskIdentifier, using: nil) { task in self.scheduleNotifications() task.setTaskCompleted(success: true) self.scheduleAppRefresh() } scheduleAppRefresh() I then schedule the task using: func scheduleAppRefresh() { let request = BGAppRefreshTaskRequest(identifier: backgroundTaskIdentifier) request.earliestBeginDate = Date(timeIntervalSinceNow: 60 * 1) do { try BGTaskScheduler.shared.submit(request) } catch { } } In my testing, I can see the background task getting called once, but if I do not launch the application during the day. The background task does not get called the next day. Is there something else I need to add to get repeated calls from the BGTaskScheduler? Thank You, JR
2
0
145
Oct ’25
BSD Privilege Escalation on macOS
This week I’m handling a DTS incident from a developer who wants to escalate privileges in their app. This is a tricky problem. Over the years I’ve explained aspects of this both here on DevForums and in numerous DTS incidents. Rather than do that again, I figured I’d collect my thoughts into one place and share them here. If you have questions or comments, please start a new thread with an appropriate tag (Service Management or XPC are the most likely candidates here) in the App & System Services > Core OS topic area. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" BSD Privilege Escalation on macOS macOS has multiple privilege models. Some of these were inherited from its ancestor platforms. For example, Mach messages has a capability-based privilege model. Others were introduced by Apple to address specific user scenarios. For example, macOS 10.14 and later have mandatory access control (MAC), as discussed in On File System Permissions. One of the most important privilege models is the one inherited from BSD. This is the classic users and groups model. Many subsystems within macOS, especially those with a BSD heritage, use this model. For example, a packet tracing tool must open a BPF device, /dev/bpf*, and that requires root privileges. Specifically, the process that calls open must have an effective user ID of 0, that is, the root user. That process is said to be running as root, and escalating BSD privileges is the act of getting code to run as root. IMPORTANT Escalating privileges does not bypass all privilege restrictions. For example, MAC applies to all processes, including those running as root. Indeed, running as root can make things harder because TCC will not display UI when a launchd daemon trips over a MAC restriction. Escalating privileges on macOS is not straightforward. There are many different ways to do this, each with its own pros and cons. The best approach depends on your specific circumstances. Note If you find operations where a root privilege restriction doesn’t make sense, feel free to file a bug requesting that it be lifted. This is not without precedent. For example, in macOS 10.2 (yes, back in 2002!) we made it possible to implement ICMP (ping) without root privileges. And in macOS 10.14 we removed the restriction on binding to low-number ports (r. 17427890). Nice! Decide on One-Shot vs Ongoing Privileges To start, decide whether you want one-shot or ongoing privileges. For one-shot privileges, the user authorises the operation, you perform it, and that’s that. For example, if you’re creating an un-installer for your product, one-shot privileges make sense because, once it’s done, your code is no longer present on the user’s system. In contrast, for ongoing privileges the user authorises the installation of a launchd daemon. This code always runs as root and thus can perform privileged operations at any time. Folks often ask for one-shot privileges but really need ongoing privileges. A classic example of this is a custom installer. In many cases installation isn’t a one-shot operation. Rather, the installer includes a software update mechanism that needs ongoing privileges. If that’s the case, there’s no point dealing with one-shot privileges at all. Just get ongoing privileges and treat your initial operation as a special case within that. Keep in mind that you can convert one-shot privileges to ongoing privileges by installing a launchd daemon. Just Because You Can, Doesn’t Mean You Should Ongoing privileges represent an obvious security risk. Your daemon can perform an operation, but how does it know whether it should perform that operation? There are two common ways to authorise operations: Authorise the user Authorise the client To authorise the user, use Authorization Services. For a specific example of this, look at the EvenBetterAuthorizationSample sample code. Note This sample hasn’t been updated in a while (sorry!) and it’s ironic that one of the things it demonstrates, opening a low-number port, no longer requires root privileges. However, the core concepts demonstrated by the sample are still valid. The packet trace example from above is a situation where authorising the user with Authorization Services makes perfect sense. By default you might want your privileged helper tool to allow any user to run a packet trace. However, your code might be running on a Mac in a managed environment, where the site admin wants to restrict this to just admin users, or just a specific group of users. A custom authorisation right gives the site admin the flexibility to configure authorisation exactly as they want. Authorising the client is a relatively new idea. It assumes that some process is using XPC to request that the daemon perform a privileged operation. In that case, the daemon can use XPC facilities to ensure that only certain processes can make such a request. Doing this securely is a challenge. For specific API advice, see this post. WARNING This authorisation is based on the code signature of the process’s main executable. If the process loads plug-ins [1], the daemon can’t tell the difference between a request coming from the main executable and a request coming from a plug-in. [1] I’m talking in-process plug-ins here. Plug-ins that run in their own process, such as those managed by ExtensionKit, aren’t a concern. Choose an Approach There are (at least) seven different ways to run with root privileges on macOS: A setuid-root executable The sudo command-line tool The authopen command-line tool AppleScript’s do shell script command, passing true to the administrator privileges parameter The osascript command-line tool to run an AppleScript The AuthorizationExecuteWithPrivileges routine, deprecated since macOS 10.7 The SMJobSubmit routine targeting the kSMDomainSystemLaunchd domain, deprecated since macOS 10.10 The SMJobBless routine, deprecated since macOS 13 An installer package (.pkg) The SMAppService class, a much-needed enhancement to the Service Management framework introduced in macOS 13 Note There’s one additional approach: The privileged file operation feature in NSWorkspace. I’ve not listed it here because it doesn’t let you run arbitrary code with root privileges. It does, however, have one critical benefit: It’s supported in sandboxed apps. See this post for a bunch of hints and tips. To choose between them: Do not use a setuid-root executable. Ever. It’s that simple! Doing that is creating a security vulnerability looking for an attacker to exploit it. If you’re working interactively on the command line, use sudo, authopen, and osascript as you see fit. IMPORTANT These are not appropriate to use as API. Specifically, while it may be possible to invoke sudo programmatically under some circumstances, by the time you’re done you’ll have code that’s way more complicated than the alternatives. If you’re building an ad hoc solution to distribute to a limited audience, and you need one-shot privileges, use either AuthorizationExecuteWithPrivileges or AppleScript. While AuthorizationExecuteWithPrivileges still works, it’s been deprecated for many years. Do not use it in a widely distributed product. The AppleScript approach works great from AppleScript, but you can also use it from a shell script, using osascript, and from native code, using NSAppleScript. See the code snippet later in this post. If you need one-shot privileges in a widely distributed product, consider using SMJobSubmit. While this is officially deprecated, it’s used by the very popular Sparkle update framework, and thus it’s unlikely to break without warning. If you only need escalated privileges to install your product, consider using an installer package. That’s by far the easiest solution to this problem. Keep in mind that an installer package can install a launchd daemon and thereby gain ongoing privileges. If you need ongoing privileges but don’t want to ship an installer package, use SMAppService. If you need to deploy to older systems, use SMJobBless. For instructions on using SMAppService, see Updating helper executables from earlier versions of macOS. For a comprehensive example of how to use SMJobBless, see the EvenBetterAuthorizationSample sample code. For the simplest possible example, see the SMJobBless sample code. That has a Python script to help you debug your setup. Unfortunately this hasn’t been updated in a while; see this thread for more. Hints and Tips I’m sure I’ll think of more of these as time goes by but, for the moment, let’s start with the big one… Do not run GUI code as root. In some cases you can make this work but it’s not supported. Moreover, it’s not safe. The GUI frameworks are huge, and thus have a huge attack surface. If you run GUI code as root, you are opening yourself up to security vulnerabilities. Appendix: Running an AppleScript from Native Code Below is an example of running a shell script with elevated privileges using NSAppleScript. WARNING This is not meant to be the final word in privilege escalation. Before using this, work through the steps above to see if it’s the right option for you. Hint It probably isn’t! let url: URL = … file URL for the script to execute … let script = NSAppleScript(source: """ on open (filePath) if class of filePath is not text then error "Expected a single file path argument." end if set shellScript to "exec " & quoted form of filePath do shell script shellScript with administrator privileges end open """)! // Create the Apple event. let event = NSAppleEventDescriptor( eventClass: AEEventClass(kCoreEventClass), eventID: AEEventID(kAEOpenDocuments), targetDescriptor: nil, returnID: AEReturnID(kAutoGenerateReturnID), transactionID: AETransactionID(kAnyTransactionID) ) // Set up the direct object parameter to be a single string holding the // path to our script. let parameters = NSAppleEventDescriptor(string: url.path) event.setDescriptor(parameters, forKeyword: AEKeyword(keyDirectObject)) // The `as NSAppleEventDescriptor?` is required due to a bug in the // nullability annotation on this method’s result (r. 38702068). var error: NSDictionary? = nil guard let result = script.executeAppleEvent(event, error: &error) as NSAppleEventDescriptor? else { let code = (error?[NSAppleScript.errorNumber] as? Int) ?? 1 let message = (error?[NSAppleScript.errorMessage] as? String) ?? "-" throw NSError(domain: "ShellScript", code: code, userInfo: nil) } let scriptResult = result.stringValue ?? "" Revision History 2025-03-24 Added info about authopen and osascript. 2024-11-15 Added info about SMJobSubmit. Made other minor editorial changes. 2024-07-29 Added a reference to the NSWorkspace privileged file operation feature. Made other minor editorial changes. 2022-06-22 First posted.
0
0
4.1k
Mar ’25
iOS26 background lock screen Blood glucose monitoring Bluetooth low energy disconnect sleep
First, our app communicates with our blood glucose monitor (CGM) using Bluetooth Low Energy (BLE). On an iPhone 14 Pro with iOS 26.0.1, Bluetooth communication works properly even when the app is in the background and locked. Even if the phone and CGM are disconnected, the app continues to scan in the background and reconnects when the phone and CGM are back in close proximity. It won't be dormant in the background or when the screen is locked. This effectively ensures that diabetic users can monitor their blood glucose levels in real time. However, after using iOS 26.0.1 on the iPhone 17, we've received user feedback about frequent disconnections in the background. Our logs indicate that Bluetooth communication is easily disconnected when switching to the background, and then easily dormant by the system, especially when the user's screen is locked. This situation significantly impacts users' blood glucose monitoring, and users are unacceptable. What can be done?
1
0
152
Oct ’25
Running processing task for data upload together with state restoration
Hi All, In continuation of this thread https://developer.apple.com/forums/thread/804439 I want to perform data upload after getting it from the BLE device. As state restoration wake should not deal with data upload i though of using a processing task to perform the data upload. So the flow will be something like: Connect to device -> listen to notification -> go to background -> wake from notification -> handle data download from ble device -> register processing task for data upload -> hopefully get the data uploaded From reading about processing task i understand that the task execution is completely handled by the OS and depends on user behaviour and app usage. I even saw that if the user is not using the app for a while, the OS might not even perfoirm the task. So my quesiton is: does state restoration wakeup and perfroming data dowloads in the backgound considered app usage that will increase the likeluhood the task will get execution time? Can we rely on this for a scenario that the user opens the app for the first time, register, onboard for ble, connect to devie and then put it in the background for days or weeks and only relying on state restoration and processing tasks to do their thing? Sorry for the long read and appreciate your support! Shimon
1
0
38
1w
Background Tasks Resources
General: Forums subtopic: App & System Services > Processes & Concurrency Forums tag: Background Tasks Background Tasks framework documentation UIApplication background tasks documentation ProcessInfo expiring activity documentation Using background tasks documentation for watchOS Performing long-running tasks on iOS and iPadOS documentation WWDC 2020 Session 10063 Background execution demystified — This is critical resource. Watch it! [1] WWDC 2022 Session 10142 Efficiency awaits: Background tasks in SwiftUI WWDC 2025 Session 227 Finish tasks in the background — This contains an excellent summary of the expected use cases for each of the background task types. iOS Background Execution Limits forums post UIApplication Background Task Notes forums post Testing and Debugging Code Running in the Background forums post Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" [1] Sadly the video is currently not available from Apple. I’ve left the link in place just in case it comes back.
0
0
3.9k
Nov ’25
How can I get a Subscriber to subscribe to get only 4 elements from an array?
Hello, I am trying to implement a subscriber which specifies its own demand for how many elements it wants to receive from a publisher. My code is below: import Combine var array = [1, 2, 3, 4, 5, 6, 7] struct ArraySubscriber<T>: Subscriber { typealias Input = T typealias Failure = Never let combineIdentifier = CombineIdentifier() func receive(subscription: any Subscription) { subscription.request(.max(4)) } func receive(_ input: T) -> Subscribers.Demand { print("input,", input) return .max(4) } func receive(completion: Subscribers.Completion<Never>) { switch completion { case .finished: print("publisher finished normally") case .failure(let failure): print("publisher failed due to, ", failure) } } } let subscriber = ArraySubscriber<Int>() array.publisher.subscribe(subscriber) According to Apple's documentation, I specify the demand inside the receive(subscription: any Subscription) method, see link. But when I run this code I get the following output: input, 1 input, 2 input, 3 input, 4 input, 5 input, 6 input, 7 publisher finished normally Instead, I expect the subscriber to only "receive" elements 1, 2, 3, 4 from the array. How can I accomplish this?
0
0
124
Aug ’25
Background Task Execution for FDA Class B Medical App Using BLE
Hello Apple Developer Community, I am developing a medical app that is classified as Class B according to FDA regulations. The app connects to a medical device using Bluetooth Low Energy (BLE) to collect critical medical data such as ECG readings. To ensure accurate data collection and maintain the quality of the medical readings, the app needs to wake up every five minutes in the background and perform tasks for approximately 30 seconds. I understand that iOS has strict limitations on background execution to preserve battery and system performance. However, due to the medical nature of the app and the need for periodic data collection, I am seeking guidance on the following: If I can provide documentation that the app is associated with an FDA-approved Class B medical device, would Apple allow more lenient background task execution policies? Are there specific APIs, such as BackgroundTasks, CoreBluetooth, or other recommended strategies, that could help me achieve this behavior reliably? Is there a process to apply for an exception or special consideration for medical apps that require periodic background activity? Any insights or recommendations would be greatly appreciated. Thank you!
2
0
330
Mar ’25
Can I ensure location is saved to SwiftData within Share Extension lifetime?
I am writing a SwiftData/SwiftUI app in which the user saves simple records, tagged with their current location. Core Location can take up to 10 seconds to retrieve the current location from its requestLocation() call. I the main app I have wrapped the CLLocationManager calls with async implementations. I kick off a Task when a new record is created, and write the location to my @Model on the main thread when it completes. A realistic use of the share extension doesn't give the task enough time to complete. I can use performExpiringActivity to complete background processing after the share extension closes but this needs to be a synchronous block. Is there some way of using performExpiringActivity when relying on a delegate callback from something like Core Location?
0
0
427
Dec ’24
How to check for cancellation of background task
When using the old withTaskCancellationHandler(operation:onCancel:isolation:) to run background tasks, you were notified that the background task gets cancelled via the handler being called. SwiftUI provides the backgroundTask(_:action:) modifier which looks quite handy. However how can I check if the background task will be cancelled to avoid being terminated by the system? I have tried to check that via Task.isCancelled but this always returns false no matter what. Is this not possible when using the modifier in which case I should file a bug report? Thanks for your help
0
0
259
Mar ’25
Help me implement SMAppServices
I have followed these steps as mentioned in this link :(https://developer.apple.com/forums/thread/721737) My projects app bundle structure is like this : TWGUI.app TWGUI.app/Contents TWGUI.app/Contents/_CodeSignature TWGUI.app/Contents/_CodeSignature/CodeResources TWGUI.app/Contents/MacOS TWGUI.app/Contents/MacOS/TWAgent TWGUI.app/Contents/MacOS/TWGUI TWGUI.app/Contents/Resources TWGUI.app/Contents/Library TWGUI.app/Contents/Library/LaunchAgents TWGUI.app/Contents/Library/LaunchAgents/com.example.TWGUI.agent.plist TWGUI.app/Contents/Info.plist TWGUI.app/Contents/PkgInfo TWGUI is my main GUI App , i which i want to embed TWAgent (a command line tool target) and register it using SMAppServices so that launchd can launch it. In TWGUI, code for registering to launchd using SMAppServices is structure as follow : import SwiftUI import ServiceManagement struct ContentView: View { let agent = SMAppService.agent(plistName: "com.example.TWGUI.agent.plist") var body: some View { VStack { Button("Register Agent") { RegisterAgent () } .padding() Button("Unregister Agent") { UnregisterAgent () } .padding() } } func RegisterAgent() { DispatchQueue.global(qos: .background).async { do { print("Registering Agent. Status: \(agent.status.rawValue)") try agent.register() print("Agent registered") } catch { print("Failed to register agent: \(error)") } } } func UnregisterAgent() { DispatchQueue.global(qos: .background).async { do { print("Unregistering Agent. Status: \(agent.status.rawValue)") try agent.unregister() print("Agent unregistered") } catch { print("Failed to unregister agent: \(error)") } } } } com.example.TWGUI.agent.plist : &lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs$ &lt;plist version="1.0"&gt; &lt;dict&gt; &lt;key&gt;Label&lt;/key&gt; &lt;string&gt;com.example.TWGUI.agent&lt;/string&gt; &lt;key&gt;ProgramArguments&lt;/key&gt; &lt;array&gt; &lt;string&gt;Contents/MacOS/TWAgent&lt;/string&gt; &lt;/array&gt; &lt;key&gt;RunAtLoad&lt;/key&gt; &lt;true/&gt; &lt;key&gt;KeepAlive&lt;/key&gt; &lt;true/&gt; &lt;/dict&gt; &lt;/plist&gt; I have used ProgramArguements instead of using Program in above plist because i was getting this error when i was using Program earlier : Registering Agent. Status: 3 Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=111 "Invalid or missing Program/ProgramArguments" UserInfo={NSLocalizedFailureReason=Invalid or missing Program/ProgramArguments} TWGUI apps Info.plist is : &lt;?xml version="1.0" encoding="UTF-8"?&gt; &lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&gt; &lt;plist version="1.0"&gt; &lt;dict&gt; &lt;key&gt;BuildMachineOSBuild&lt;/key&gt; &lt;string&gt;23C71&lt;/string&gt; &lt;key&gt;CFBundleDevelopmentRegion&lt;/key&gt; &lt;string&gt;en&lt;/string&gt; &lt;key&gt;CFBundleExecutable&lt;/key&gt; &lt;string&gt;TWGUI&lt;/string&gt; &lt;key&gt;CFBundleIdentifier&lt;/key&gt; &lt;string&gt;com.example.TWAgent&lt;/string&gt; &lt;key&gt;CFBundleInfoDictionaryVersion&lt;/key&gt; &lt;string&gt;6.0&lt;/string&gt; &lt;key&gt;CFBundleName&lt;/key&gt; &lt;string&gt;TWGUI&lt;/string&gt; &lt;key&gt;CFBundlePackageType&lt;/key&gt; &lt;string&gt;APPL&lt;/string&gt; &lt;key&gt;CFBundleShortVersionString&lt;/key&gt; &lt;string&gt;1.0&lt;/string&gt; &lt;key&gt;CFBundleSupportedPlatforms&lt;/key&gt; &lt;array&gt; &lt;string&gt;MacOSX&lt;/string&gt; &lt;/array&gt; &lt;key&gt;CFBundleVersion&lt;/key&gt; &lt;string&gt;1&lt;/string&gt; &lt;key&gt;DTCompiler&lt;/key&gt; &lt;string&gt;com.apple.compilers.llvm.clang.1_0&lt;/string&gt; &lt;key&gt;DTPlatformBuild&lt;/key&gt; &lt;string&gt;&lt;/string&gt; &lt;key&gt;DTPlatformName&lt;/key&gt; &lt;string&gt;macosx&lt;/string&gt; &lt;key&gt;DTPlatformVersion&lt;/key&gt; &lt;string&gt;14.2&lt;/string&gt; &lt;key&gt;DTSDKBuild&lt;/key&gt; &lt;string&gt;23C53&lt;/string&gt; &lt;key&gt;DTSDKName&lt;/key&gt; &lt;string&gt;macosx14.2&lt;/string&gt; &lt;key&gt;DTXcode&lt;/key&gt; &lt;string&gt;1510&lt;/string&gt; &lt;key&gt;DTXcodeBuild&lt;/key&gt; &lt;string&gt;15C65&lt;/string&gt; &lt;key&gt;LSMinimumSystemVersion&lt;/key&gt; &lt;string&gt;14.2&lt;/string&gt; &lt;/dict&gt; &lt;/plist&gt; TWAgent target has main.swift file which does this : import Foundation let startTime = CFAbsoluteTimeGetCurrent() func logTimeSinceStart() { let elapsedTime = CFAbsoluteTimeGetCurrent() - startTime NSLog("Time since program started: \(elapsedTime) seconds") } func startLoggingTime() { Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { _ in logTimeSinceStart() } } // Start logging time startLoggingTime() // Keep the run loop running CFRunLoopRun() I followed these exact same steps in another project earlier and my agent was getting registered, although i lost that project due to some reasons. But now i am getting this error when i am registering or unregistering agent using SMAppServices from the code above : Registering Agent. Status: 3 Failed to register agent: Error Domain=SMAppServiceErrorDomain Code=1 "Operation not permitted" UserInfo={NSLocalizedFailureReason=Operation not permitted} I tried diffrent fixes for like this : Moved app bundle to /applications folder Gave permission for full disc access to this app . Code sign again (both agent and TWGUI ... But nothing seems to work , getting same error. I tried to launch agent using : Launchctl load com.example.TWGUI.agent.plist and it worked , so there is no issue with my plist implementation. Can someone help me understand how can i solve this issue ? or if i am following right steps ? Can give steps need to follow to implement this and steps so that i can register and start my agent using SMAppServices? And i also tried the project give in apples official documentation : [https://developer.apple.com/documentation/servicemanagement/updating-your-app-package-installer-to-use-the-new-service-management-api) but got same error in this project as well .
2
0
132
Apr ’25
How to safely maximize concurrent UI rendering
I'm using Swift 6 and tasks to concurrently process multiple PDF files for rendering, and it's working well. But currently I'm manually limiting the number of simultaneous tasks to 2 out of fear that the system might run many tasks concurrently without having enough RAM to do the PDF processing. Testing on a variety of devices, I've tried increasing the task limit and haven't seen any crashes, but I'm quite concerned about the possibility. Any given device might be using a lot of RAM at any moment, and any given PDF might strain resources more than the average PDF. Is there a recommended technique for handling this kind of scenario? Should I not worry about it and just go ahead and start a high number of tasks, trusting that the system won't run too many concurrently and therefore won't run out of RAM?
2
0
266
Mar ’25
Did GCD change in macOS 26
Some users of my Mac app are complaining of redrawing delays. Based on what I see in logs, my GCD timer event handlers are not being run in a timely manner although the runloop is still pumping events: sometimes 500ms pass before a 15ms timer runs. During this time, many keypresses are routed through -[NSApplication sendEvent:], which is how I know it's not locked up in synchronous code. This issue has not been reported in older versions of macOS. I start the timer like this: _gcdUpdateTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue()); dispatch_source_set_timer(_gcdUpdateTimer, dispatch_time(DISPATCH_TIME_NOW, period * NSEC_PER_SEC), period * NSEC_PER_SEC, 0.0005 * NSEC_PER_SEC); dispatch_source_set_event_handler(_gcdUpdateTimer, ^{ …redraw… });
1
0
104
Sep ’25
Waiting for an Async Result in a Synchronous Function
This comes up over and over, here on the forums and elsewhere, so I thought I’d post my take on it. If you have questions or comments, start a new thread here on the forums. Put it in the App & System Services > Processes & Concurrency subtopic and tag it with Concurrency. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Waiting for an Async Result in a Synchronous Function On Apple platforms there is no good way for a synchronous function to wait on the result of an asynchronous function. Lemme say that again, with emphasis… On Apple platforms there is no good way for a synchronous function to wait on the result of an asynchronous function. This post dives into the details of this reality. Prime Offender Imagine you have an asynchronous function and you want to call it from a synchronous function: func someAsynchronous(input: Int, completionHandler: @escaping @Sendable (_ output: Int) -> Void) { … processes `input` asynchronously … … when its done, calls the completion handler with the result … } func mySynchronous(input: Int) -> Int { … calls `someAsynchronous(…)` … … waits for it to finish … … results the result … } There’s no good way to achieve this goal on Apple platforms. Every approach you might try has fundamental problems. A common approach is to do this working using a Dispatch semaphore: func mySynchronous(input: Int) -> Int { fatalError("DO NOT WRITE CODE LIKE THIS") let sem = DispatchSemaphore(value: 0) var result: Int? = nil someAsynchronous(input: input) { output in result = output sem.signal() } sem.wait() return result! } Note This code produces a warning in the Swift 5 language mode which turns into an error in the Swift 6 language mode. You can suppress that warning with, say, a Mutex. I didn’t do that here because I’m focused on a more fundamental issue here. This code works, up to a point. But it has unavoidable problems, ones that don’t show up in a basic test but can show up in the real world. The two biggest ones are: Priority inversion Thread pools I’ll cover each in turn. Priority Inversion Apple platforms have a mechanism that helps to prevent priority inversion by boosting the priority of a thread if it holds a resource that’s needed by a higher-priority thread. The code above defeats that mechanism because there’s no way for the system to know that the threads running the work started by someAsynchronous(…) are being waited on by the thread blocked in mySynchronous(…). So if that blocked thread has a high-priority, the system can’t boost the priority of the threads doing the work. This problem usually manifests in your app failing to meet real-time goals. An obvious example of this is scrolling. If you call mySynchronous(…) from the main thread, it might end up waiting longer than it should, resulting in noticeable hitches in the scrolling. Threads Pools A synchronous function, like mySynchronous(…) in the example above, can be called by any thread. If the thread is part of a thread pool, it consumes a valuable resource — that is, a thread from the pool — for a long period of time. The raises the possibility of thread exhaustion, that is, where the pool runs out of threads. There are two common thread pools on Apple platforms: Dispatch Swift concurrency These respond to this issue in different ways, both of which can cause you problems. Dispatch can choose to over-commit, that is, start a new worker thread to get work done while you’re hogging its existing worker threads. This causes two problems: It can lead to thread explosion, where Dispatch starts dozens and dozens of threads, which all end up blocked. This is a huge waste of resources, notably memory. Dispatch has an hard limit to how many worker threads it will create. If you cause it to over-commit too much, you’ll eventually hit that limit, putting you in the thread exhaustion state. In contrast, Swift concurrency’s thread pool doesn’t over-commit. It typically has one thread per CPU core. If you block one of those threads in code like mySynchronous(…), you limit its ability to get work done. If you do it too much, you end up in the thread exhaustion state. WARNING Thread exhaustion may seem like just a performance problem, but that’s not the case. It’s possible for thread exhaustion to lead to a deadlock, which blocks all thread pool work in your process forever. There’s a trade-off here. Swift concurrency doesn’t over-commit, so it can’t suffer from thread explosion but is more likely deadlock, and vice versa for Dispatch. Bargaining Code like the mySynchronous(…) function shown above is fundamentally problematic. I hope that the above has got you past the denial stage of this analysis. Now let’s discuss your bargaining options (-: Most folks don’t set out to write code like mySynchronous(…). Rather, they’re working on an existing codebase and they get to a point where they have to synchronously wait for an asynchronous result. At that point they have the choice of writing code like this or doing a major refactor. For example, imagine you’re calling mySynchronous(…) from the main thread in order to update a view. You could go down the problematic path, or you could refactor your code so that: The current value is always available to the main thread. The asynchronous code updates that value in an observable way. The main thread code responds to that notification by updating the view from the current value. This refactoring may or may not be feasible given your product’s current architecture and timeline. And if that’s the case, you might end up deploying code like mySynchronous(…). All engineering is about trade-offs. However, don’t fool yourself into thinking that this code is correct. Rather, make a note to revisit this choice in the future. Async to Async Finally, I want to clarify that the above is about synchronous functions. If you have a Swift async function, there is a good path forward. For example: func mySwiftAsync(input: Int) async -> Int { let result = await withCheckedContinuation { continuation in someAsynchronous(input: input) { output in continuation.resume(returning: output) } } return result } This looks like it’s blocking the current thread waiting for the result, but that’s not what happens under the covers. Rather, the Swift concurrency worker thread that calls mySwiftAsync(…) will return to the thread pool at the await. Later, when someAsynchronous(…) calls the completion handler and you resume the continuation, Swift will grab a worker thread from the pool to continue running mySwiftAsync(…). This is absolutely normal and doesn’t cause the sorts of problems you see with mySynchronous(…). IMPORTANT To keep things simple I didn’t implement cancellation in mySwiftAsync(…). In a real product it’s important to support cancellation in code like this. See the withTaskCancellationHandler(operation:onCancel:isolation:) function for the details.
0
0
772
Oct ’25
What happens after BGContinuedProcessingTask "expires"?
If I create a BGContinuedProcessingTaskRequest, register it, and then "do work" within it appropriately reporting progress, and before my task has finished doing all the work it had to do, its expirationHandler triggers... does the task later try again? Or does it lose the execution opportunity until the app is next re-launched to the foreground? In my testing, I never saw my task execute again once expired (which suggests the latter?). I was able to easily force this expiry by starting my task, backgrounding my app, then launching the iOS Camera App. My example is just using test code inspired from https://developer.apple.com/documentation/backgroundtasks/performing-long-running-tasks-on-ios-and-ipados let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: "Video Upload", subtitle: "Starting Upload") request.strategy = .queue BGTaskScheduler.shared.register(forTaskWithIdentifier: taskIdentifier, using: nil) { task in guard let task = task as? BGContinuedProcessingTask else { return } print("i am a good task") var wasExpired = false task.expirationHandler = { wasExpired = true } let progress = task.progress progress.totalUnitCount = 100 while !progress.isFinished && !wasExpired { progress.completedUnitCount += 1 let formattedProgress = String(format: "%.2f", progress.fractionCompleted * 100) task.updateTitle(task.title, subtitle: "Completed \(formattedProgress)%") sleep(1) } if progress.isFinished { print ("i was a good task") task.setTaskCompleted(success: true) } else { print("i was not a good task") task.setTaskCompleted(success: false) } } try? BGTaskScheduler.shared.submit(request) Apologies if this is clearly stated somewhere and I'm missing it.
1
0
66
Nov ’25
Background Assets Extension and DeviceCheck
Hi, I have some questions regarding the Background Assets Extension and DeviceCheck framework. Goal: Ensure that only users who have purchased the app can access the server's API without any user authentication using for example DeviceCheck framework and within a Background Assets Extension. My app relies on external assets, which I'm loading using the Background Assets Extension. I'm trying to determine if it's possible to obtain a challenge from the server and send a DeviceCheck assertion during this process within the Background Assets Extension. So far, I only receive session-wide authentication challenges—specifically NSURLAuthenticationMethodServerTrust in the Background Assets Extensio. I’ve tested with Basic Auth (NSURLAuthenticationMethodHTTPBasic) just for experimentation, but the delegate func backgroundDownload( _ download: BADownload, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) is never called with that authentication method. It seems task-specific challenges aren't coming through at all. Also, while the DCAppAttestService API appears to be available on macOS, DCAppAttestService.isSupported always returns false (in my testing), which suggests it's not actually supported on macOS. Can anyone confirm if that’s expected behavior?
2
0
140
May ’25
Processes & Concurrency Resources
General: DevForums subtopic: App & System Services > Processes & Concurrency Processes & concurrency covers a number of different technologies: Background Tasks Resources Concurrency Resources — This includes Swift concurrency. Service Management Resources XPC Resources Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com"
0
0
166
Jul ’25
Concurrency Resources
Swift Concurrency Resources: Forums tags: Concurrency The Swift Programming Language > Concurrency documentation Migrating to Swift 6 documentation WWDC 2022 Session 110351 Eliminate data races using Swift Concurrency — This ‘sailing on the sea of concurrency’ talk is a great introduction to the fundamentals. WWDC 2021 Session 10134 Explore structured concurrency in Swift — The table that starts rolling out at around 25:45 is really helpful. Swift Async Algorithms package Swift Concurrency Proposal Index DevForum post Why is flow control important? forums post Matt Massicotte’s blog Dispatch Resources: Forums tags: Dispatch Dispatch documentation — Note that the Swift API and C API, while generally aligned, are different in many details. Make sure you select the right language at the top of the page. Dispatch man pages — While the standard Dispatch documentation is good, you can still find some great tidbits in the man pages. See Reading UNIX Manual Pages. Start by reading dispatch in section 3. WWDC 2015 Session 718 Building Responsive and Efficient Apps with GCD [1] WWDC 2017 Session 706 Modernizing Grand Central Dispatch Usage [1] Avoid Dispatch Global Concurrent Queues forums post Waiting for an Async Result in a Synchronous Function forums post Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" [1] These videos may or may not be available from Apple. If not, the URL should help you locate other sources of this info.
0
0
1.9k
Oct ’25