0

I have a Cocoa app based on NSDocument that presents text documents to the user. Document contents are read on a background queue, which causes a problem:

I use NSAttributedString with images, i.e. it can contain NSTextAttachment and NSTextAttachmentCell. When I try to initialize an attachment for an image and I have the main thread checker activated in Xcode, I get the following error:

// On background queue:
let attachment = NSTextAttachment()
attachment.attachmentCell = NSTextAttachmentCell(imageCell: image) <-

"[NSCell init] must be used from main thread only"

My first attempt was to wrap that code in DispatchQueue.main.sync {}, but this caused a deadlock with NSDocument once in a while when autosaving took place or when the user saved the document.

Autosaving would block the main queue, my code would run in the background trying to read the document, but this ended up in a deadlock, because I could not call out to the main queue to create the text attachment.

My question:

Is it possible for me to ignore the main thread checker in Xcode and instantiate NSTextAttachmentCell on a background queue anyway?

All I'm doing on the background queue is initializing the attributed string with its attachments. Further modifications are made on the main queue.

Sequence of events

  1. Thread 2 (bg queue): Need to update abc.txt for some reason. Get read/write access to abc.txt via NSFileCoordinator
    • Thread 2 (bg) now in NSFileCoordinator block
  2. Thread 1 (MAIN): User-initiated NSDocument save, NSDocument requests write access to abc.txt via NSFileCoordinator
    • Thread 1 (MAIN) now blocked, waiting for file coordinator lock of Thread 2
  3. Thread 2 (bg queue): Moving along in file coordinator block..., trying to initialize NSAttributedString, oh, it contains an attachment, can't initialize NSTextAttachmentCell on background queue, let me hand this off to the MAIN queue real' quick... ⚡️ DEADLOCK ⚡️
    • Thread 2 (bg) is now waiting for Thread 1 (MAIN), which is waiting in front of its file coordinator access block for Thread 2 (bg) to finish with its file coordinator block.
Mark
  • 6,647
  • 1
  • 45
  • 88

1 Answers1

0

You should not ignore the main thread warnings. If you use main.async rather than sync there should never be an issue with adding the attachment. This reference also helps out defining which classes behave well in different threads. Generally speaking, any AppKit view type class should only be used on the main thread.

Lucas Derraugh
  • 6,929
  • 3
  • 27
  • 43
  • Thanks for the link. I did manage to produce a deadlock, even with `main.async` when using file coordination: (1) my background operation obtains a read/write lock on the file using file coordination and starts to read the file, (2) NSDocument begins an auto-save also using file coordination and blocks the main queue, (3) on my background queue, my code tries to instantiate the `NSAttributedString` with the attachment cell by offloading that to the (blocked) main queue. Not sure how to solve this... – Mark Jun 08 '20 at 15:46
  • Does the NSDocument auto-save block the main queue forever? #3 sounds irrelevant to the problem. Maybe this is a case for trying to avoid the autosave functionality: https://stackoverflow.com/questions/44161308/prevent-nsdocuments-auto-saving-while-its-content-is-editing – Lucas Derraugh Jun 08 '20 at 18:19
  • Yes, NSDocument saving causes a deadlock with my background queue and hence locks the main queue forever. Disabling auto-save is not an option, unfortunately. I added a few Thread.sleep() at the right places to reproduce this reliably and noticed that a user-save via Cmd-S causes the same problem. The core issue is that both, NSDocument and my background operation, use NSFileCoordinator to get read/write access to the file and then the background operation is forced to reach out to the (NSDocument-blocked) main queue in order to initiate the NSTextAttachmentCell. – Mark Jun 09 '20 at 08:46
  • I added the exact sequence of events to the question. – Mark Jun 09 '20 at 09:00