NotificationCenter delivery order for concurrent multi-threaded posts
0 reputation · 25 Jan 2024, 08:19 UTC
0 reputation · 25 Jan 2024, 08:19 UTC
Apple's Foundation NotificationCenter manages the distribution of notifications to registered observers. When notifications are posted from a single thread, the delivery sequence is predictable. However, a design challenge arises when multiple threads post notifications concurrently to the same center.
In environments such as iOS 17 or macOS 14, the framework does not specify a guaranteed delivery order for notifications originating from different threads. This nondeterministic behavior can lead to race conditions if the application logic assumes a specific sequence of events across these threads.
Given that the framework provides no native API to enforce global ordering across concurrent posts, what is the recommended architectural pattern to ensure a deterministic sequence without introducing deadlocks? Is there a documented configuration to serialize delivery across disparate posting threads?
31275 reputation · 25 Jan 2024, 13:29 UTC
There is no documented Apple API that serialises delivery of notifications posted from different threads. The only guarantee is that the call to post is synchronous on the caller’s thread – the observers that were registered with a non‑nil queue are executed asynchronously on that queue, but the order in which post calls finish is still undefined when multiple threads are involved.
post calls are executed by the OS scheduler.queue, the callback runs asynchronously on that queue. That does not affect the ordering of the post calls themselves.post synchronously on a serial queue from within an observer that is already executing on that same queue will deadlock. Using async to enqueue the post avoids this problem.The most straightforward way to obtain a deterministic sequence is to funnel every post through a single serial execution context (a DispatchQueue or an actor). Within that context you can optionally attach a monotonically increasing sequence number or timestamp to each notification before it is forwarded to NotificationCenter. Because the context is serial, the order in which the notifications leave that context is the order in which they will be delivered to observers.
Example using a serial DispatchQueue:
final class OrderedNotifier {
private let queue = DispatchQueue(label: "com.example.ordered-notifier")
private let center = NotificationCenter.default
func post(name: Notification.Name, object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) {
// All callers dispatch to the same serial queue.
queue.async {
let info = userInfo ?? [:]
// Optional: add a sequence number if you need to verify order.
self.center.post(name: name, object: object, userInfo: info)
}
}
}
When an observer is registered with a non‑nil queue, it will still run asynchronously on that queue, but the order of notifications it receives will match the order in which OrderedNotifier enqueued them.
Alternatively, with Swift Concurrency you can use a single actor that serialises all posts:
actor OrderedNotifier {
private let center = NotificationCenter.default
func post(name: Notification.Name, object: Any? = nil, userInfo: [AnyHashable: Any]? = nil) async {
// The actor guarantees that posts are executed in the order the method is called.
let info = userInfo ?? [:]
center.post(name: name, object: object, userInfo: info)
}
}
Callers simply await the actor method; the actor’s single execution context enforces ordering without blocking the caller’s thread.
post calls deterministic across threads without an external sequence. If you need a strict order, you must attach a sequence number or timestamp yourself and sort before delivery.addObserver(forName:object:queue:using:) with a dedicated concurrent queue) to keep the serial path free.NotificationCenter with a typed stream; for very high throughput you might consider AsyncStream or Combine’s PassthroughSubject as an alternative, but those also require a single producer to preserve order.OrderedNotifier.post with a unique sequence number in the userInfo dictionary.OrderedNotifier.post again. The call should be enqueued asynchronously and not block the observer.What is the expected peak number of post calls per second and the typical amount of work each observer performs? If the throughput is very high or observers do heavy processing, the serial queue may become a performance limiter and you might need to switch to a typed async sequence or Combine subject that can handle high‑rate producers while preserving ordering.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.