Re: NSDocument Serialization (-performSynchronousFileAccessUsingBlock: and friends)
Re: NSDocument Serialization (-performSynchronousFileAccessUsingBlock: and friends)
- Subject: Re: NSDocument Serialization (-performSynchronousFileAccessUsingBlock: and friends)
- From: Kyle Sluder <email@hidden>
- Date: Thu, 29 Sep 2011 16:41:37 -0700
On Thu, Sep 29, 2011 at 12:33 PM, Kevin Perry <email@hidden> wrote:
>
> On Sep 29, 2011, at 12:00 PM, Kyle Sluder wrote:
>
>> On Thu, Sep 29, 2011 at 9:20 AM, Kevin Perry <email@hidden> wrote:
>>> If it were to call the fileAccessCompletionHandler any earlier then it might
>>> be possible, for example, for -fileModificationDate to be invoked on the
>>> main thread after -writeSafelyToURL: has written the file, but before the
>>> value has been properly updated.
>>
>> Thank you, this is enlightening. Your sketch omitted the "take a
>> snapshot of the document wrapper" part; I assume that comes prior to
>> the call to -performAsynchronousFileAccess…, but still within the same
>> enveloping call to -performActivityWithSynchronousWaiting:….
>
> For Versions, you mean? That also happens within -performAsynchronousFileAccess:, because we don't want anything else touching the file while we're preserving the version.
I was referring to -fileWrapperOfType:, which is described as being a
snapshot of the document's contents somewhere in the headerdocs. It's
the same thing you refer to at the end of your reply, so I think we're
on the same page here.
>> However, I am still concerned about blocking other apps. According to
>> the documentation, -performAsynchronousFileAccessUsingBlock: does not
>> block the main thread. But it surely has to block _something_ until
>> the fileAccessCompletionHandler is called.
>
> Yeah, it blocks further invocations of -perform(A)synchronousFileAccessUsingBlock:, where "block" means "enqueue" for async access, and "block the thread" for sync access.
Okay, so that explains how the following worked out for me.
I decided to try out what happened if I called NSRunAlertPanel from
within an entirely synchronous activity
(-performActivityWithSynchronousWaiting:YES, containing a call to
-performSynchronousFileActivity). Surprisingly, that didn't cause
deadlock:
- (IBAction)doActivity:(id)sender {
[self performActivityWithSynchronousWaiting:YES usingBlock:^(void
(^activityCompletionHandler)(void)){
[self performSynchronousFileAccessUsingBlock:^{
/* ... [[NSFileManager defaultManager] muckAbout:] ... */
}];
// Present an app-modal panel and don't return until the user clicks OK
NSRunAlertPanel(@"Deadlock?", @"We should be deadlocked.",
@"OK", nil, nil);
activityCompletionHandler();
}];
}
Surprisingly to me at first, this did not deadlock. We're inside a
call to -performActivityWithSynchronousWaiting:, so my mind kept
thinking "blocking the main thread" meant that I would not receive
user events to dismiss that panel. But of course, NSRunAlertPanel runs
the runloop. What _does_ get "blocked" (or "enqueued") are any calls
to -perform(A)SynchronousActivity that happen to be triggered by
-presentError:'s running of the runloop. Specifically, any calls to
-performSynchronousActivity block the thread, resulting in deadlock:
- (void)deadlockMe:(id)unused;
{
[self performActivityWithSynchronousWaiting:YES usingBlock:^(void
(^activityCompletionHandler)(void)){
NSLog(@"DEADLOCK?");
activityCompletionHandler();
}];
NSLog(@"WE ESCAPED DEADLOCK!");
}
- (IBAction)doActivity:(id)sender {
[self performActivityWithSynchronousWaiting:YES usingBlock:^(void
(^activityCompletionHandler)(void)){
[self performSynchronousFileAccessUsingBlock:^{
/* ... [[NSFileManager defaultManager] muckAbout:] ... */
}];
[self performSelector:@selector(deadlockMe:) withObject:nil
afterDelay:0 inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
// Present an app-modal panel and don't return until the user clicks OK
NSRunAlertPanel(@"Deadlock?", @"We should be deadlocked.",
@"OK", nil, nil);
activityCompletionHandler();
}];
}
I think the source of confusion is overloading of the term "block." In
addition to referring to the block of code passed to
-performActivityWithSynchronousWaiting:, as well as actual blocking of
a thread via synchronization primitives such as semaphores, the
NSDocument headerdocs have introduced a new definition that means
"conceptually enqueue this activity and wait for its completion". Two
activities can become deadlocked on a single thread by attempting to
reentrantly call -performActivityWithSynchronousWaiting:YES. This
deadlock can block the thread's execution.
Case in point, here's where the terminology starts to get confusing:
>> Does
>> "-performAsynchronousFileAccess does not block the main thread" really
>> mean "if I can't execute the block now, I will execute it later, and
>> in doing so the thread on which this method was called (which might be
>> the main thread) will be blocked until the block has finished
>> executing _and_ the fileAccessCompletionHandler has been called"?
>
> No, because blocking the thread until fileAccessCompletionHandler is called would defeat the purpose of asynchronous saving. The thread is unblocked immediately after the block finishes executing. The main thread will only be blocked if performSynchronousFileAccess: is called later and the async file access hasn't yet completed.
This description disagrees with the headerdoc, which says that
-performAsynchronousFileAccess never blocks the main thread. So
"blocked" cannot mean the traditional threading sense. It must mean
"prohibiting other calls to -perform(A)SynchronousFileAccess from
executing".
In other words, -performAsynchronousFileAccess will attempt to take
the "file access" mutex, enqueueing its block argument on the queue of
waiting file accessors if it cannot claim the mutex. Once the enqueued
block argument begins executing, it holds the "file access" mutex
until such time as it calls its fileActivityCompletionHandler.
-performSynchronousFileAccess will attempt to take the "file access"
mutex, blocking the current thread until its block argument is able to
obtain the mutex and finishes executing. All enqueued asynchronous
accesses are allowed to execute before -performSynchronousFileAccess
is granted the "file access" mutex.
This sounds an awful lot like a monitor with two queues.
>> All -performActivityWithSynchronousWaiting:usingBlock: is doing here
>> is wrapping a call to -performAsynchronousFileAccessUsingBlock:, which
>> in turn is just wrapping a call to dispatch_async. Do I need to
>> reimplement my own analogue to -unblockUserInteraction, to ensure the
>> user doesn't make any changes to the document while the move operation
>> is taking place on my backgroundQueue? Or is that scenario specific to
>> the conflict between "save the document asynchronously" vs "manipulate
>> the document's data?"
>
> Why do you care if the user is modifying the document in memory while you're playing around with the file in the background? The only reason we block the main thread with async saving is because we need to get the snapshot of the in-memory state to write it to disk on the background thread. Once we have that snapshot, we can touch the file all we want while the user merrily continues editing the document in memory.
I think my improved understanding of the distinction between "blocked
on a thread" and "taking the file access mutex" has resolved this
question for me.
A general issue I have with asynchronous saving: what if the save
operation fails? The user has now made additional changes, but their
"Save a Version" operation did not create a version for them. So now
they can't roll back to the timepoint they thought they created.
>
>> Sorry to bombard you with questions, but the headerdoc only goes so far.
>
> As you discover things that you think aren't documented well, please give back and file doc bugs. The more specific you can get, the better.
I'm going to ruminate on how I think the headerdoc's terminology could
be cleared up, and how the entire system could be better-described,
and file a radar.
This entire scheme is really asking developers to understand a lot. I
don't mean to trivialize the issue, since multi-process file
coordination is going to require some complexity. But the NSDocument
learning curve seems to have changed shape from a smooth if steep
climb to a vertical cliff that begins at
-saveToURL:ofType:forSaveOperation:completionHandler:. A major part of
the problem is the inability to see the call sites of all the blocks
that get enqueued during the normal course of NSDocument operations.
It is very difficult to chase message flow in this new world order.
Thanks for your hands-on help in demystifying a lot of this for me.
--Kyle Sluder
_______________________________________________
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