GO FURTHER, FASTER: Try the Swift Career Accelerator today! >>

Using SwiftData with a Widget leads to deadlock

Forums > SwiftUI

Hello,

I was relieved to read this post about the ability to use SwiftData inside a widget. I am accessing SwiftData from both the TimelineProvider and also the widget's view. However, the widget's view sometimes does not update anymore and attaching Xcode to the process shows a deadlock when the widget's view is trying to access one of my model in SwiftData. Is anyone else experiencing the same problem? Thanks!

Here are two snippets of the two threads in the widget being stuck: Thread 1:

#0  0x00000001fc714c2c in kevent_id ()
#1  0x00000001bda85b10 in _dispatch_kq_poll ()
#2  0x00000001bda864e0 in _dispatch_event_loop_wait_for_ownership ()
#3  0x00000001bda72984 in __DISPATCH_WAIT_FOR_QUEUE__ ()
#4  0x00000001bda7254c in _dispatch_sync_f_slow ()
#5  0x00000001bdc997f8 in -[NSManagedObjectContext performBlockAndWait:] ()
#6  0x00000001bdd13e70 in NSManagedObjectContext.performAndWait<τ_0_0>(_:) ()
#7  0x000000024f2b36f8 in static _DefaultBackingData.getValue<τ_0_0>(key:managedObject:for:) ()
#8  0x000000024f2b7150 in _DefaultBackingData.getValue<τ_0_0>(forKey:) ()
#9  0x000000024f2bb870 in protocol witness for BackingData.getValue<τ_0_0>(forKey:) in conformance _DefaultBackingData<τ_0_0> ()
#10 0x000000024f26ead0 in PersistentModel.getValue<τ_0_0>(forKey:) ()
****#11 0x0000000104328534 in StopDepartureTime.id.getter at /var/folders/p1/1xbpjpjd4r105gb3srs3y1g00000gn/T/swift-generated-sources/@swiftmacro_15DepartureWidget04StopA4TimeC2id18_PersistedPropertyfMa_.swift:10
#12 0x000000010432bd20 in protocol witness for Identifiable.id.getter in conformance StopDepartureTime ()**
#13 0x00000001baaa8c2c in ___lldb_unnamed_symbol181142 ()
#14 0x00000001af13895c in RawKeyPathComponent._projectReadOnly<τ_0_0, τ_0_1, τ_0_2>(_:to:endingWith:) ()
#15 0x00000001af1380b8 in closure #2 in KeyPath._projectReadOnly(from:) ()
#16 0x00000001af31cffc in partial apply for closure #2 in KeyPath._projectReadOnly(from:) ()
#17 0x00000001af1374c4 in AnyKeyPath.withBuffer<τ_0_0>(_:) ()
#18 0x00000001af137d70 in KeyPath._projectReadOnly(from:) ()
#19 0x00000001af13bd0c in swift_getAtKeyPath ()
#20 0x00000001baaa7ec4 in ___lldb_unnamed_symbol181113 ()

And thread 2:

#0  0x00000001fc714d08 in __psynch_mutexwait ()
#1  0x000000021ed1ed68 in _pthread_mutex_firstfit_lock_wait ()
#2  0x000000021ed1e7f0 in _pthread_mutex_firstfit_lock_slow ()
#3  0x00000001b97f84bc in _MovableLockLock ()
#4  0x00000001ba34d348 in ___lldb_unnamed_symbol123836 ()
#5  0x0000000258bd0568 in partial apply for closure #4 in closure #1 in static ObservationTracking._installTracking(_:willSet:didSet:) ()
#6  0x0000000258bd0680 in partial apply for thunk for @escaping @callee_guaranteed @Sendable () -> () ()
#7  0x0000000258bce878 in specialized ObservationRegistrar.Context.willSet<τ_0_0, τ_0_1>(_:keyPath:) ()
#8  0x0000000258bcf26c in specialized ObservationRegistrar.willSet<τ_0_0, τ_0_1>(_:keyPath:) ()
#9  0x0000000258bca1b8 in ObservationRegistrar.withMutation<τ_0_0, τ_0_1, τ_0_2>(of:keyPath:_:) ()
#10 0x000000024f560ac0 in ___lldb_unnamed_symbol494 ()
#11 0x000000024f560960 in ___lldb_unnamed_symbol492 ()
#12 0x000000024f561798 in ___lldb_unnamed_symbol513 ()
#13 0x0000000258bca1cc in ObservationRegistrar.withMutation<τ_0_0, τ_0_1, τ_0_2>(of:keyPath:_:) ()
#14 0x000000024f560ac0 in ___lldb_unnamed_symbol494 ()
#15 0x000000024f5610b8 in ___lldb_unnamed_symbol500 ()
#16 0x000000024f5611c4 in ___lldb_unnamed_symbol501 ()
#17 0x00000001b5b64c78 in __CFNOTIFICATIONCENTER_IS_CALLING_OUT_TO_AN_OBSERVER__ ()
#18 0x00000001b5b64640 in ___CFXRegistrationPost_block_invoke ()
#19 0x00000001b5b64588 in _CFXRegistrationPost ()
#20 0x00000001b5b63ad8 in _CFXNotificationPost ()
#21 0x00000001b4afb7e4 in -[NSNotificationCenter postNotificationName:object:userInfo:] ()

3      

Were you able to resolve your issue?

I have encountered a similar problem and it seems that the Observation registrar (or rather whatever what SwiftUI is doing with it) is the culprict here. To my observation (and minimal example), the problem arises as soon when you try to protect your @Observable storage using mutual exclusion.

Referring to the stack trace below, the value getter will be waiting for the lock of the setter to be released while the setter is somewhere deadlocked within the ObservationRegistrar withMutation call. The deadlock happens presumambly somewhere as part of the observation tracking. Checking against the open-source implementation of the Observation framework, I couldn't find a reason why it would deadlock there. Therefore the assumption, that SwiftUI is deadlocking itself somehow. Most logical explanation would be, that SwiftUI tries to acquire a in the observation tracking onChange handler that is currently hold when executing the View body. So a classic Lock Order Inversion.

So the questions: Do you always need to make sure that custom locking mechanisms are within the withMutation(keyPath:_:) call? Does SwiftUI account for that? Does it guarantee that it doesn't call the property getter before the mutation is completed (the public ObservationTracking interface only exposes a willSet change handler, didSet is exclusive to SwiftUI)? Lastly, was this fixed in SwiftData?

struct TestDeadLockView: View {
    @Observable
    final class ObservableTest: Sendable {
        private nonisolated(unsafe) var _value = 0 // observed property
        let lock = NSLock()

        var value: Int {
            get {
                lock.withLock {
                    _value // implicitily calls access(keyPath:)
                }
            }
            set {
                lock.withLock {
                    _value = newValue // implicitily calls withMutation(keyPath:_:)
                }
            }
        }
    }

    @State var test = ObservableTest()

    var body: some View {
        List {
            HStack {
                Text("Content")
                Spacer()
                Text("\(test.value)")
            }
            Button("Run") {
                Task.detached { [test] in
                    for amount in 1..<100 {
                        usleep(50)
                        test.value += 1
                    }
                }
            }
        }
    }
}

Main Thread:

Thread 1 Queue : com.apple.main-thread (serial)
#0  0x00000001d877ecb4 in __psynch_mutexwait ()
#1  0x00000001ec51efa8 in _pthread_mutex_firstfit_lock_wait ()
#2  0x00000001ec51e9b8 in _pthread_mutex_firstfit_lock_slow ()
#3  0x0000000106568d98 in NSLocking.withLock<NSLock>(_:) ()
#4  0x0000000106568b18 in TestDeadLockView.ObservableTest.value.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:52
#5  0x0000000106569e0c in closure #1 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:69
#6  0x0000000106a55920 in ListRow.init(_:content:) at /Users/andi/Library/Developer/Xcode/DerivedData/UITests-fekuzmpvqcfiushfnpeypinhaado/SourcePackages/checkouts/SpeziViews/Sources/SpeziViews/Views/List/ListRow.swift:103
#7  0x0000000106a55b28 in ListRow.init<>(_:content:) at /Users/andi/Library/Developer/Xcode/DerivedData/UITests-fekuzmpvqcfiushfnpeypinhaado/SourcePackages/checkouts/SpeziViews/Sources/SpeziViews/Views/List/ListRow.swift:93
#8  0x0000000106569a28 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:68
#9  0x00000001942688cc in ___lldb_unnamed_symbol143772 ()
#10 0x00000001065697d4 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:67
#11 0x000000010656a7c8 in protocol witness for View.body.getter in conformance TestDeadLockView ()
#12 0x0000000193696528 in ___lldb_unnamed_symbol62086 ()
#13 0x00000001936e1738 in ___lldb_unnamed_symbol63606 ()
#14 0x00000001936df3bc in ___lldb_unnamed_symbol63534 ()
#15 0x0000000193691b30 in ___lldb_unnamed_symbol61998 ()
#16 0x00000001b82e9010 in AG::Graph::UpdateStack::update ()
#17 0x00000001b82e8bfc in AG::Graph::update_attribute ()
#18 0x00000001b82e87d8 in AG::Subgraph::update ()
#19 0x00000001936fb068 in ___lldb_unnamed_symbol64017 ()
#20 0x00000001936f9500 in ___lldb_unnamed_symbol63998 ()
#21 0x00000001936f7d7c in ___lldb_unnamed_symbol63982 ()
#22 0x00000001936f7b0c in ___lldb_unnamed_symbol63980 ()
#23 0x000000019360cc6c in ___lldb_unnamed_symbol59064 ()
#24 0x00000001918d6a4c in -[UIView(CALayerDelegate) layoutSublayersOfLayer:] ()
#25 0x0000000190d353b4 in CA::Layer::layout_if_needed ()
#26 0x0000000190d34f38 in CA::Layer::layout_and_display_if_needed ()
#27 0x0000000190d900e0 in CA::Context::commit_transaction ()
#28 0x0000000190d05028 in CA::Transaction::commit ()
#29 0x0000000190d4ed7c in CA::Transaction::flush_as_runloop_observer ()
#30 0x000000019197fff4 in _UIApplicationFlushCATransaction ()
#31 0x000000019197d76c in _UIUpdateSequenceRun ()
#32 0x000000019197d3b0 in schedulerStepScheduledMainSection ()
#33 0x000000019197e254 in runloopSourceCallback ()
#34 0x000000018f69b834 in __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ ()
#35 0x000000018f69b7c8 in __CFRunLoopDoSource0 ()
#36 0x000000018f699298 in __CFRunLoopDoSources0 ()
#37 0x000000018f698484 in __CFRunLoopRun ()
#38 0x000000018f697cd8 in CFRunLoopRunSpecific ()
#39 0x00000001d45481a8 in GSEventRunModal ()
#40 0x0000000191cd090c in -[UIApplication _run] ()
#41 0x0000000191d849d0 in UIApplicationMain ()
#42 0x0000000193888148 in ___lldb_unnamed_symbol74307 ()
#43 0x0000000193834714 in ___lldb_unnamed_symbol71237 ()
#44 0x00000001938404d0 in ___lldb_unnamed_symbol71675 ()
#45 0x000000010656cbb8 in static UITestsApp.$main() ()
#46 0x000000010656d068 in main at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:85
#47 0x00000001b2d49e4c in start ()

The concurent thread:

Thread 12 Queue : com.apple.root.default-qos.cooperative (concurrent)
#0  0x00000001d877ecb4 in __psynch_mutexwait ()
#1  0x00000001ec51efa8 in _pthread_mutex_firstfit_lock_wait ()
#2  0x00000001ec51e9b8 in _pthread_mutex_firstfit_lock_slow ()
#3  0x000000019353c3d4 in _MovableLockLock ()
#4  0x00000001938625b4 in ___lldb_unnamed_symbol72967 ()
#5  0x000000023e01875c in merged partial apply forwarder for closure #4 @Sendable () -> () in closure #1 (Observation.ObservationTracking.Entry) -> Observation.ObservationTracking.Id in static Observation.ObservationTracking._installTracking(_: Observation.ObservationTracking, willSet: Swift.Optional<@Sendable (Observation.ObservationTracking) -> ()>, didSet: Swift.Optional<@Sendable (Observation.ObservationTracking) -> ()>) -> () ()
#6  0x000000023e018874 in partial apply forwarder for reabstraction thunk helper from @escaping @callee_guaranteed @Sendable () -> () to @escaping @callee_guaranteed @Sendable () -> (@out ()) ()
#7  0x000000023e017044 in function signature specialization <Arg[0] = Dead> of Observation.ObservationRegistrar.Context.willSet<τ_0_0, τ_0_1 where τ_0_0: Observation.Observable>(_: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>) -> () ()
#8  0x000000023e01757c in function signature specialization <Arg[0] = Dead> of Observation.ObservationRegistrar.willSet<τ_0_0, τ_0_1 where τ_0_0: Observation.Observable>(_: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>) -> () ()
#9  0x000000023e011e88 in Observation.ObservationRegistrar.withMutation<τ_0_0, τ_0_1, τ_0_2 where τ_0_0: Observation.Observable>(of: τ_0_0, keyPath: Swift.KeyPath<τ_0_0, τ_0_1>, _: () throws -> τ_0_2) throws -> τ_0_2 ()
#10 0x000000010656886c in TestDeadLockView.ObservableTest.withMutation<Int, ()>(keyPath:_:) at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/@__swiftmacro_7TestApp0A12DeadLockViewV010ObservableA00F0fMm_.swift:13
#11 0x000000010656858c in TestDeadLockView.ObservableTest._value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/@__swiftmacro_7TestApp0A12DeadLockViewV010ObservableA0C6_value33_B4AA65106754BF17E8E6D1CDD7B2EB6BLL18ObservationTrackedfMa_.swift:11
#12 0x0000000106568e54 in closure #1 in TestDeadLockView.ObservableTest.value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:58
#13 0x0000000106568e84 in partial apply for closure #1 in TestDeadLockView.ObservableTest.value.setter ()
#14 0x0000000106568da8 in NSLocking.withLock<NSLock>(_:) ()
#15 0x0000000106568ca0 in TestDeadLockView.ObservableTest.value.setter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:57
#16 0x000000010656a3b0 in closure #1 in closure #2 in closure #1 in TestDeadLockView.body.getter at /Users/andi/XcodeProjects/Stanford/SpeziBluetooth/Tests/UITests/TestApp/TestApp.swift:76
#17 0x000000010656fc64 in partial apply for closure #1 in closure #2 in closure #1 in TestDeadLockView.body.getter ()
#18 0x00000001065451c0 in thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) ()
#19 0x000000010656fe78 in partial apply for thunk for @escaping @isolated(any) @callee_guaranteed @Sendable @async () -> (@out A) ()

   

Followup to the previous reply. If you switch the locking and call to withMutation(keyPath:_:) you might encounter inconsistent view updates. So, is it just not possible to use @Observable from anywhere else than the Main Actor? Refer to the minimal example below:

struct TestDeadLockView: View {
    @Observable
    final class ObservableTest: Sendable {
        @ObservationIgnored private nonisolated(unsafe) var _value = 0
        let lock = NSLock()

        var value: Int {
            get {
                access(keyPath: \._value)
                return lock.withLock {
                    _value
                }
            }
            set {
                withMutation(keyPath: \._value) {
                    lock.withLock {
                        _value = newValue
                    }
                }
            }
        }
    }

    @State var test = ObservableTest()
    @State var buttonPresses = 0

    var body: some View {
        List {
            HStack {
                Text("Content")
                Spacer()
                Text("\(test.value)")
            }
            HStack {
                Text("Button Presses")
                Spacer()
                Text("\(buttonPresses)")
            }
            Button("Run") {
                buttonPresses += 1
                Task.detached { [test] in
                    usleep(UInt32.random(in: 150..<500))
                    test.value += 1 // The view will not always update after this write
                }
            }
        }
    }
}

   

Hacking with Swift is sponsored by Essential Developer.

SPONSORED Transform your career with the iOS Lead Essentials. Unlock over 40 hours of expert training, mentorship, and community support to secure your place among the best devs. Click for early access to this limited offer and a FREE crash course.

Click to save your free spot now

Sponsor Hacking with Swift and reach the world's largest Swift community!

Reply to this topic…

You need to create an account or log in to reply.

All interactions here are governed by our code of conduct.

 
Unknown user

You are not logged in

Log in or create account
 

Link copied to your pasteboard.