Question

I'm fairly new to objective C. If I have a class property that will likely be getting modified during asynchronous events like API calls, what is the best way to make sure that changing the property while it is being accessed by another thread will not cause a crash?

As far as I can tell I have two options:

1) NSLock + atomic property

...but it seems like in this case I would have to lock the property for every read and write, which to me would defeat the purpose of setting it as atomic.

2) Nonatomic property

I could also set it as nonatomic, but then I think I would have to do all reading/writing on the main thread. Is there a way of doing that as a result of an API call? Would a call back to a delegate after a successful API response be on the thread opened for that API call, or would it be back on the main thread? And if it's on a different thread, could I put it back on the main thread? Specifically, I'm worried about an NSArray getting changed while another thread is looping through it.

What is the best way of doing this?

Was it helpful?

Solution

I would like to grab justin's option "dispatch APIs" for a short example:

Concurrent access to shared resources can be made safe through executing all accesses on a dedicated serial queue, lets call it "sync_queue".

This "sync_queue" will likely be a private queue of the class whose ivars is the resource you want to modify.

You may now define a read/write nonatomic property, for example:

@propery (nonatomic) NSArray* array;

The write access can be implemented as shown below:

- (void) setArray:(NSArray* newValue) 
{
    dispatch_async(sync_queue, ^{
        _array = newValue;
    });
}

Note that a write access is asynchronous.

The read access to the property will be implemented as follows:

- (NSArray*) array:(NSArray* value) 
{
    if (dispatch_get_specific(SyncQueueID) == sync_queue_id)) {
        return _array;
    }
    else {
        __block NSArray* result = nil;
        dispatch_sync(_sync_queue, ^{
            result = _array;
        });
        return result;
    }     
}

Unlike a write access, a read access requires to be synchronous. The method also has to check if the current execution context is not already the sync_queue or a child or any grand children of the sync queue - otherwise, the read access would cause a dead lock.

To identify the current execution context we associate a particular identifier with the sync queue, using function dispatch_queue_set_specific() when creating it. Later we use dispatch_get_specific to obtain this identifier from the current queue or from the parent or any grand parent queue. If it returns this particular identifier, the method is executing on the sync queue respectively on a child queue or any grand children. If that's true, the method returns the value immediately. Otherwise, it synchronously schedules on the sync queue.

Note:

If the shared resource will be accessed by UIKit, the sync_queue shall be the main queue.

OTHER TIPS

It's more like 3 options:

  1. NSLock
  2. atomic property
  3. nonatomic property

1) yes, you would have to lock the property for every read and write. This does give you the flexibility to lock around a whole iteration rather than just each access to the cellection.

2) everything just works for access to the variable (but, you could generate a situation where an array was mutated while being iterated as there is no lock around the iteration as a whole)

3) yes, you could do all reading/writing by calling back to the main thread. Whether delegate methods are called on the processing thread or the main thread depends on the call you make / the framework you use. You can switch back to the main thread with GCD or a perform selector.

If I have a class property that will likely be getting modified during asynchronous events like API calls, what is the best way to make sure that changing the property while it is being accessed by another thread will not cause a crash?

For mutable objects, you will need some form of mutual exclusion. There are many options, depending on abstraction level and usage. Examples of:

  • pthread_mutex* APIs
  • NSLock APIs
  • @synchronized
  • semaphores
  • dispatch APIs

1) NSLock + atomic property ...but it seems like in this case I would have to lock the property for every read and write, which to me would defeat the purpose of setting it as atomic.

Exactly. If you need to lock every access, Atomic offers nothing. It's very rare that atomic is actually useful (a corner case where a property is so simple and also independent of any other state).

In more detail, you have mentioned NSArray. If that is a copy property (and it should be), then atomic could in rare cases allow you to safely get/set the array by immutable copy in practice. However, it's not very useful in practice to have a class which is no more than a pointer to an immutable array instance; usually, you want to do something with that array, and usually you want to interact with the objects in a thread safe manner. The implication there is the lock in question can also be used for mutual exclusion to the array's elements (if done correctly).

So where do you need to lock to guarantee mutual exclusion of an NSMutableArray ivar? When setting, when getting, and nearly every time you message it. Even asking its -count or for its elements should involve a lock to eliminate any race condition. Of course, you can wrap this up in higher level operations for correctness and do those operations -- acquiring the lock once.

2) Nonatomic property

Atomic will not save you, nor will nonatomic. Atomic only saves you from some potential race conditions in this scenario. Therefore, you should generally use nonatomic because you already need to introduce full mutual exclusion to guarantee there is no race condition.

Would a call back to a delegate after a successful API response be on the thread opened for that API call, or would it be back on the main thread?

That depends on the API.

And if it's on a different thread, could I put it back on the main thread?

Yes, you could add it to the run loop of the main thread or use the dispatch queue. That's 'kludgey', unless the work needs to happen on a specific thread -- most obvious case is when updating an AppKit or UIKit view.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top