Surprising entanglement of UI lock during fetch request in background thread (NSViewHierarchyLock/MOC locking deadlock).
Surprising entanglement of UI lock during fetch request in background thread (NSViewHierarchyLock/MOC locking deadlock).
- Subject: Surprising entanglement of UI lock during fetch request in background thread (NSViewHierarchyLock/MOC locking deadlock).
- From: Luke Evans <email@hidden>
- Date: Fri, 20 Feb 2009 11:57:12 -0800
Hi list,
I have a nasty deadlock caused by the condition lock used by -
[NSViewHierarachyLock _lockForWriting:handler:] never getting acquired
on a non-main thread. This is on 10.5.6 BTW.
I can sort of see what's going on, but I'm not sure how one is
supposed to fix it.
Here's what's happening:
My app has a table whose content shows files. The files that get
shown here are stored in Core Data, the table is bound to the Core
Data model to show the appropriate stuff.
A background thread is started to spider through the file system
looking for appropriate files. When a new candidate file is spotted,
the managed object context is locked, and the file is added to the
'store'.
Naturally, when a new item arrives in the MOC, the table that is bound
to it tries to update. This is when the deadlock occurs.
The state of the two threads when things lock up appears as follows:
The background thread has acquired the MOC lock, which minimally wraps
a call to -[NSManagedObjectContext executeFetchRequest:error:]. It is
doing a fetch request to see if a file that is being processed by the
file system walker is already in the MOC.
Here's this thread's stack:
--------------------
#0 0x94fef3ae in __semwait_signal
#1 0x9501a326 in _pthread_cond_wait
#2 0x95019d0d in pthread_cond_wait$UNIX2003
#3 0x937e6c77 in -[NSViewHierarchyLock _lockForWriting:handler:]
#4 0x937e6827 in -[NSViewHierarchyLock
lockForWritingWithExceptionHandler:]
#5 0x931b7b79 in -[NSView setFrameSize:]
#6 0x931ae00a in -[NSControl setFrameSize:]
#7 0x932adcff in -[NSTableView setFrameSize:]
#8 0x932ad0c7 in -[NSTableView tile]
#9 0x932c6b05 in -[NSTableView rowsInRect:]
#10 0x933d8138 in -[NSTableBinder _visibleRowIndexesForObject:]
#11 0x933d79e7 in -[NSTableBinder
observeValueForKeyPath:ofObject:change:context:]
#12 0x96e00b0e in NSKVONotify
#13 0x96d910a5 in -[NSObject(NSKeyValueObservingPrivate)
_notifyObserversForKeyPath:change:]
#14 0x931b670a in -[NSController _notifyObserversForKeyPath:change:]
#15 0x931b660b in -[NSController didChangeValueForKey:]
#16 0x933d2920 in -[NSArrayController
didChangeValuesForArrangedKeys:objectKeys:indexKeys:]
#17 0x933d5780 in -[NSArrayController
_arrangeObjectsWithSelectedObjects:avoidsEmptySelection:operationsMask:useBasis
:]
#18 0x933d06b6 in -[NSArrayController setContent:]
#19 0x936cf1a9 in -[_NSManagedProxy _managedObjectsChangedInContext:]
#20 0x96d7fe1a in _nsnote_callback
#21 0x94f078da in __CFXNotificationPost
#22 0x94f07bb3 in _CFXNotificationPostNotification
#23 0x96d7d080 in -[NSNotificationCenter
postNotificationName:object:userInfo:]
#24 0x96595439 in -
[NSManagedObjectContext(_NSInternalNotificationHandling)
_postObjectsDidChangeNotificationWithUserInfo:]
#25 0x96595302 in -
[NSManagedObjectContext(_NSInternalChangeProcessing)
_createAndPostChangeNotification:withDeletions:withUpdates:withRefreshes
:]
#26 0x9659415b in -
[NSManagedObjectContext(_NSInternalChangeProcessing)
_processRecentChanges:]
#27 0x96592e70 in -[NSManagedObjectContext executeFetchRequest:error:]
--------------------
I note here that while processing the fetch request on the MOC, the
MOC chooses to notify its observers, which includes the NSTableBinder
for table that shows the files.
Apparently, the table is unable to refresh its view because...
Over on the main thread, back at the ranch...
...the table is drawing, and guess what: it want to access the MOC to
get the name of the file it's drawing. Because accessing this
attribute is a potentially faulting operation, this too is protected
by a lock on the MOC, which is never acquired.
Here's the main thread stack:
---------------------
#0 0x94fe8202 in semaphore_wait_trap
#1 0x94fefcf2 in pthread_mutex_lock
#2 0x9658bdf5 in -[_PFLock lock]
#3 0x9658bdca in -[NSManagedObjectContext lock]
#4 0x00007849 in -[MYFileTracker managerForFile:] at MYFileTracker.m:261
#5 0x00008831 in -[MYFileTracker identifyingDisplayTextForFile:] at
MYFileTracker.m:523
#6 0x0000da26 in -[MYFile identifyingDisplayText] at MYFile.m:78
#7 0x96dd15fa in -[NSObject(NSKeyValueCoding) valueForKeyPath:]
#8 0x933947f0 in -[NSBinder
_valueForKeyPath:ofObject:mode:raisesForNotApplicableKeys:]
#9 0x9346caed in -[NSBinder
valueForBinding:atIndex:resolveMarkersToPlaceholders:]
#10 0x9346c91d in -[NSValueBinder _referenceBindingValueAtIndex:]
#11 0x933c98d3 in -[NSValueBinder
_adjustObject:mode:observedController:observedKeyPath:context:editableState:adjustState
:]
#12 0x9346c78c in -[NSValueBinder
updateTableColumnDataCell:forDisplayAtIndex:]
#13 0x9346c69d in -[NSTextValueBinder
updateTableColumnDataCell:forDisplayAtIndex:]
#14 0x9346c639 in -[_NSBindingAdaptor tableColumn:willDisplayCell:row:]
#15 0x932e17a1 in -[NSTableView
_sendBindingAdapterWillDisplayCell:forColumn:row:]
#16 0x93263ab3 in -[NSTableView preparedCellAtColumn:row:]
#17 0x9326387c in -[NSTableView
_drawContentsAtRow:column:withCellFrame:]
#18 0x93262db2 in -[NSTableView drawRow:clipRect:]
#19 0x932081a0 in -[NSTableView drawRowIndexes:clipRect:]
#20 0x93206c84 in -[NSTableView drawRect:]
#21 0x9329729c in -[NSView _drawRect:clip:]
#22 0x93295d93 in -[NSView
_recursiveDisplayAllDirtyWithLockFocus:visRect:]
#23 0x9329612a in -[NSView
_recursiveDisplayAllDirtyWithLockFocus:visRect:]
#24 0x9329612a in -[NSView
_recursiveDisplayAllDirtyWithLockFocus:visRect:]
#25 0x932946e9 in -[NSView
_recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView
:]
#26 0x93295543 in -[NSView
_recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView
:]
#27 0x93295543 in -[NSView
_recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView
:]
#28 0x9329402b in -[NSThemeFrame
_recursiveDisplayRectIfNeededIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:topView
:]
#29 0x93290b4f in -[NSView
_displayRectIgnoringOpacity:isVisibleRect:rectIsVisibleRectForView:]
#30 0x931d1523 in -[NSView displayIfNeeded]
#31 0x931d10d1 in -[NSWindow displayIfNeeded]
#32 0x931d0ef4 in _handleWindowNeedsDisplay
#33 0x94f249a2 in __CFRunLoopDoObservers
#34 0x94f25cfc in CFRunLoopRunSpecific
#35 0x94f26cd8 in CFRunLoopRunInMode
#36 0x955d12c0 in RunCurrentEventLoopInMode
#37 0x955d10d9 in ReceiveNextEventCommon
#38 0x955d0f4d in BlockUntilNextEventMatchingListInMode
#39 0x931ced7d in _DPSNextEvent
#40 0x931ce630 in -[NSApplication
nextEventMatchingMask:untilDate:inMode:dequeue:]
#41 0x931c766b in -[NSApplication run]
#42 0x931948a4 in NSApplicationMain
#43 0x00003844 in main at main.m:13
--------------------
So, in a nutshell, we have the following dual-lock deadlock:
- The table is drawing in the main thread and therefore holds some
internal lock protecting view hierarchy modification. It is trying to
acquire the managed object context lock to obtain some data to render.
- The 'spider' thread is simply checking if a file already exists in
the managed object context. It acquires what it thinks is a very
shortly-held lock on the MOC to bracket a fetch request, but
unexpectedly, the act of simply interrogating the
managed object context (executing a fetch request), causes a
notification to the table informing it of some recent change, which in
turn requires the view hierarchy lock being held by the main thread.
I was a little surprised at this turn of events having conformed (so I
thought) to one of the models multi-threaded code using Core Data.
The docs suggest locking a single MOC is an acceptable usage in a
multi-threaded environment so long as you are completely diligent with
the locks. I figure from the docs that the preferred method is "MOC
per thread", but opted for the locking.
Clearly, if the MOC locks had not been there, the app would not have
deadlocked in this case. However, the real 'culprit' here (or maybe
I'll just say 'surprise') is that executing a fetch request on the MOC
causes the table to require its view hierarchy lock.
Questions that come to mind:
- Is there any way to suppress the table notification in the non-main
thread when I'll issuing the fetch request?
- Can I force it to happen before I lock the MOC to execute the fetch
request? Seems like that wouldn't be a strong guarantee of proper
functioning, but it might considerably reduce the potential for the
NSKVONotify to be posted to the table right in the middle of my fetch
request! Is this something that -commitEditing might help with... but
then surely I'm supposed to lock the MOC around that operation too,
and that would likely produce the same result.
- What methods have people found to solve this? I don't think I'm
missing anything pertinent from the docs... but you never know ;-)
- Does this suggest that the MOC-locking approach to Core Data in a
multi-threaded environment isn't, actually, practical (or at least not
in the presence of UI binding to the MOC)?
I can certainly look at changing over to a MOC-per-thread approach
(though reading the docs that sounds like a bit of a pain). However,
right now I'd prefer to continue with the locking method if it can be
made to work in the presence of UI binding to core data - which the
documentation gives no guidance on, while suggesting locking as a
viable approach in general.
Suggestions greatly appreciated!!
-- lwe
_______________________________________________
Cocoa-dev mailing list (email@hidden)
Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com
Help/Unsubscribe/Update your Subscription:
This email sent to email@hidden