Behavior differences between performBlock: and performBlockAndWait:?
Asked Answered
A

1

31

I'm creating an NSManagedObjectContext in a private queue to handle data updates I take from files and/or services:

NSManagedObjectContext *privateContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
AppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
privateContext.persistentStoreCoordinator = appDelegate.persistentStoreCoordinator;

Since I'm using a private queue, I don't fully understand the difference between performBlock: and performBlockAndWait: methods... To perform my data updates I'm currently doing this:

[privateContext performBlock: ^{

        // Parse files and/or call services and parse
        // their responses

        // Save context
        [privateContext save:nil];

        dispatch_async(dispatch_get_main_queue(), ^{
            // Notify update to user
        });
    }];

In this case, my data updates are made synchronoulsy and sequentially, so I suppose that is the correct place to save the context, right? If I'm doing something wrong, I'd appreciate if you let me know. On the other hand, would this code be equivalent?:

[privateContext performBlockAndWait: ^{

        // Parse files and/or call services and parse
        // their responses

        // Save context
        [privateContext save:nil];
    }];

// Notify update to user

Again I guess that is the correct place to save the context... what are the differences between both methods (if any, in this case)?

What if instead of performing synchronous service calls or files parsing, I need to perform asynchronous service calls? How would these data updates be managed?

Thanks in advance

Acidulous answered 25/8, 2015 at 8:6 Comment(0)
S
90

You are correct in that anything you want to do with a MOC must be done within either performBlock or performBlockAndWait. Note that retain/release is thread safe for managed objects, so you don't have to be inside one of those blocks to retain/release reference counts on managed objects.

They both utilize a synchronous queue to process messages, which means that only one block will execute at a time. Well, that's almost true. See the descriptions of performBlockAndWait. In any event, the access to the MOC will be serialized such that only one thread is accessing the MOC at a time.

tl;dr Don't worry about the difference, and always use performBlock.

Factual Differences

There are a number of differences. I'm sure there are more, but here are the ones that I think are most important to understand.

Synchronous vs. Asynchronous

performBlock is asynchronous, in that it returns immediately, and the block is executed at some time in the future, on some undisclosed thread. All blocks given to the MOC via performBlock will execute in the order they were added.

performBlockAndWait is synchronous, in that the calling thread will wait until the block has executed before returning. Whether the block runs in some other thread, or runs in the calling thread is not all that important, and is an implementation detail that can't be trusted.

Note, however, that it could be implemented as "Hey, some other thread, go run this block. I'm gonna sit here doing nothing until you tell me it's done." Or, it could be implemented as "Hey, Core Data, give me a lock that prevents all those other blocks from running so I can run this block on my own thread." Or it could be implemented in some other way. Again, implementation detail, which could change at any time.

I'll tell you this though, the last time I tested it, performBlockAndWait executed the block on the calling thread (meaning the second option in the above paragraph). This is only really information to help you understand what is going on, and should not be relied upon in any way.

Reentrancy

performBlock is always asynchronous, and is thus not reentrant. Well, some may consider it reentrant, in that you can call it from within a block that was called with performBlock. However, if you do this, all calls to performBlock will return immediately, and the block will not execute until at least the currently executing block completely finishes its work.

[moc performBlock:^{
    doSomething();
    [moc performBlock:^{
      doSomethingElse();
    }];
    doSomeMore();
}];

These functions will always be executed in this order:

doSomething()
doSomeMore()
doSomethingElse()

performBlockAndWait is always synchronous. Furthermore, it is also reentrant. Multiple calls will not deadlock. Thus, if you end up calling performBlockAndWait while you are inside a block that was being run as a result of another performBlockAndWait, then it's OK. You will get the expected behavior, in that the second call (and any subsequent calls) will not cause a deadlock. Furthermore, the second one will completely execute before it returns, as you would expect.

[moc performBlockAndWait:^{
    doSomething();
    [moc performBlockAndWait:^{
      doSomethingElse();
    }];
    doSomeMore();
}];

These functions will always be executed in this order:

doSomething()
doSomethingElse()
doSomeMore()

FIFO

FIFO stands for "First In First Out" which means that blocks will be executed in the order in which they were put into the internal queue.

performBlock always honors the FIFO structure of the internal queue. Every block will be inserted into the queue, and only run when it is removed, in FIFO order.

By definition, performBlockAndWait breaks FIFO ordering because it jumps the queue of blocks that have already been enqueued.

Blocks submitted with performBlockAndWait do not have to wait for other blocks that are running in the queue. There are a number of ways to see this. One simple one is this.

[moc performBlock:^{
    doSomething();
    [moc performBlock:^{
      doSomethingElse();
    }];
    doSomeMore();
    [moc performBlockAndWait:^{
      doSomethingAfterDoSomethingElse();
    }];
    doTheLastThing();
}];

These functions will always be executed in this order:

doSomething()
doSomeMore()
doSomethingAfterDoSomethingElse()
doTheLastThing()
doSomethingElse()

It's obvious in this example, which is why I used it. Consider, however, if your MOC is getting stuff called on it from multiple places. Could be a bit confusing.

The point to remember though, is that performBlockAndWait is preemptive and can jump the FIFO queue.

Deadlock

You will never get a deadlock calling performBlock. If you do something stupid inside the block, then you could deadlock, but calling performBlock will never deadlock. You can call it from anywhere, and it will simply add the block to the queue, and execute it some time in the future.

You can easily get deadlocks calling performBlockAndWait, especially if you call it from a method that an external entity can call or indiscriminately within nested contexts. Specifically, you are almost guaranteed to deadlock your applications if a parent calls performBlockAndWait on a child.

User Events

Core Data considers a "user event" to be anything between calls to processPendingChanges. You can read the details of why this method is important, but what happens in a "user event" has implications on notifications, undo management, delete propagation, change coalescing, etc.

performBlock encapsulates a "user event" which means the block of code is automatically executed between distinct calls to processPendingChanges.

performBlockAndWait does not encapsulate a "user event." If you want the block to be treated as a distinct user event, you must do it yourself.

Auto Release Pool

performBlock wraps the block in its own autoreleasepool.

performBlockAdWait does not provide a unique autoreleasepool. If you need one, you must provide it yourself.

Personal Opinions

Personally, I do not believe there are very many good reasons to use performBlockAndWait. I'm sure someone has a use case that can't be accomplished in any other way, but I've yet to see it. If anyone knows of that use case, please share it with me.

The closest is calling performBlockAndWait on a parent context (don't ever do this on a NSMainConcurrencyType MOC because it could lock up your UI). For example, if you want to ensure that the database has completely saved to disk before the current block returns and other blocks get a chance to run.

Thus, a while ago, I decided to treat Core Data as a completely asynchronous API. As a result, I have a whole lot of core data code, and I do not have one single call to performBlockAndWait, outside of tests.

Life is much better this way. I have much fewer problems than I did back when I thought "it must be useful or they wouldn't have provided it."

Now, I simply no longer have any need for performBlockAndWait. As a result, maybe it has changed some, and I just missed it because it no longer interests me... but I doubt that.

Spokane answered 25/8, 2015 at 21:40 Comment(10)
Great answer. With performBlock:, if you need to update the UI with a property of a managed object, do you typically just dispatch to main at the end of the block?` (Using dispatch_async or -[NSOperationQueue mainQueue]?Hydrokinetic
@JasonMoore - I dispatch_async to the main thread. I like to use an easier-to-use function that I write as a simple wrapper around dispatch_async, so I don't have to specify the queue... it's a little easier to read.Spokane
@JodyHagins without using performBlockAndWait, how are you know when the core data is finished saving? is notification the only solution then?Kramlich
@TonyLin You put your code in performBlock - in that block you do your save and then whatever else you want to do afterwards - basically you chain actions together.Spokane
Hi Jody. I read your answer, but have a different question. Can you see How do you maintain object integrity when you have nested (child/parent) contexts?Showers
I love your answer Jody. I did find a use fo performBlockAndWait. I created many operations that need to complete only when their work has been finished since I keep data in sync this way. I don't want the next Operation to continue in the queue since they really on the fact that the data has been synced to continue it work. In simpler words, I used it in an Operation that is a dependant to another operation.Trusty
Thanks Jody. I noticed that you said you used performBlockAndWait in tests. I have some tests that use this as well. However I'm seeing tests crash intermittently. Could they be related?Foppish
@Foppish Yes, it could be related. You must be very careful, even in tests.Spokane
@JodyHagins I have found that performBlockAndWait only breaks FIFO ordering for main queue contexts. For a private queue context, it will execute the previously queued blocks first.Aronarondel
My CoreData API methods are using call back(aka. @escaping closure) to pass the result to caller. When I replace them from privateMOC.performAndWait to privateMOC.perform, my project failed, sometimes entities fetch results returned with empty and sometime app crash. I would look into it but if anyone met this problem similar? My CoreData API call is hereHygro

© 2022 - 2024 — McMap. All rights reserved.