user3521551
user3521551

Reputation: 21

Correct way to use MagicalRecord in a concurrent NSOperation (MagicalRecord-2.3)

With MR_contextForCurrentThread not being safe for Operations (and being deprecated), Im trying to ensure I understand the best pattern for series of read/writes in a concurrent operations.

It's been advised to use saveWithBlock for storing new records, and presumably deletion, which provides a context for use. The Count and fetch methods can be given a context, but still use MR_contextForCurrentThread by default.

Is the safest pattern to obtain a context using [NSManagedObjectContext MR_context] at the start of the operation, and use it for all actions. The operation depends on some async work, but not long running. Then perform a MR_saveToPersistentStoreWithCompletion when the operation is finished?

Upvotes: 2

Views: 908

Answers (2)

Bohdan Orlov
Bohdan Orlov

Reputation: 304

You can't use saveWithBlock from multiple threads (concurrent NSOperations) if you want to:

  • rely on create by primary attribute feature of Magical Record
  • rely on automatic establishment of relationships (which relies on primary attribute)
  • manually fetch/MR_find objects and do save based on result of it

This is because whenever you use saveWithBlock new local context created, so that multiple context created in the same time and they don't know about changes in each other. As Tony mentioned localContext is a snapshot of rootContext and changes goes only in one direction, from localContext to rootContext, but not vice versa.

Here is thread-save (or even consistency-safe in terms of MagicalRecord) method that synchronizes calls to saveWithBlock:

@implementation MagicalRecord (MyActions)
+ (void) my_saveWithBlock:(void(^)(NSManagedObjectContext *localContext))block completion:(MRSaveCompletionHandler)completion;
{
    static dispatch_semaphore_t semaphore;
    static dispatch_once_t once;
    dispatch_once(&once, ^{
        semaphore = dispatch_semaphore_create(1);
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
        [MagicalRecord saveWithBlock:block
            completion:^(BOOL success, NSError *error) {
                dispatch_semaphore_signal(semaphore);
                if (completion){
                    completion(success, error);
                }
            }];
    });
}
@end

Upvotes: 1

Tony Arnold
Tony Arnold

Reputation: 2929

What's the reason for using an NSOperation? There are two options here:

Use MagicalRecord's background saving blocks:

[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
    // Do your task for the background thread here
}];

The other option is (as you've already tried) to bundle it up into an NSOperation. Yes, I would cache an instance of a private queue context using [NSManagedObjectContext MR_newContext] (sorry, I deprecated the MR_context method this afternoon in favour of a clearer alternative). Be aware that unless you manually merge changes from other contexts, the private queue context that you create will be a snapshot of the parent context at the point in time that you created it. Generally that's not a problem for short running background tasks.

Managed Object Contexts are really lightweight and cheap to create — whenever you're about to do work on any thread other than the main thread, just initialise and use a new context. It keeps things simple. Personally, I favour the + saveWithBlock: and associated methods — they're just simple.

Hope that helps!

Upvotes: 2

Related Questions