Chris
Chris

Reputation: 556

What is the correct way to synchronize this multithreaded code (iOS/Cocoa)?

Let's say I have an NSMutableArray of objects (NSMutableArray is not thread-safe), and I have these methods on an object that contains this array (this is a simplified example for the sake of clarity):

- (void)addObject:(id)object {
    if (_objectsArray == nil) {
        _objectsArray = [NSMutableArray array];
    }

    [_objectsArray addObject:object];

    if (_thread == nil) {
        _thread = [[NSThread alloc] initWithTarget:self selector:@selector(__threadEntry:) object:nil];
        _thread.name = @"com.company.ThreadName";
        [_thread start];
    }
}

- (void)removeObject:(id)object {
    [_objectsArray removeObject:object];

    if (_objectsArray.count == 0) {
        _isRunning = NO;
    }
}

- (void)stopRendering {
    _isRunning = NO;
}

- (void)__threadEntry:(id)sender {
    // Set up CADisplayLink on current run loop.

    // "_isRunning" is declared as a "volatile BOOL"
    _isRunning = YES;
    while (_isRendering) {
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    // Thread cleanup.
}

- (void)__threadProc {
    @autoreleasepool {
        [_objectsArray enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
            // Do work
        }];
    }
}

So basically, I have methods that add/remove objects from the mutable array, but work on the objects in the array is performed on a different thread. i.e. addObject and removeObject are both only called from the main thread, whereas the work (in __threadProc) is done on a different thread.

As it is, this code is not thread-safe, as an object can be added/removed while enumeration is under progress in __threadProc. So what is the correct way to synchronize this?

I'm not sure if locks is the right answer here, because do locks work across different methods? For example, if I put a lock/unlock around [_objectsArray addObject:object] in the addObject method and a lock/unlock around the work in __threadProc, would that work (assuming of course that both are the same lock object (e.g. NSLock)?

Also, adding/removing objects happens very infrequently compared to how often work is done in __threadProc.

Upvotes: 1

Views: 1187

Answers (1)

BuLB JoBs
BuLB JoBs

Reputation: 861

Suppose that we’re implementing a thread-safe queue in Objective-C. We might start it like this:

@implementation ThreadSafeQueue
{
   NSMutableArray * _objectsArray;
   NSLock *_lock;
}

- (instancetype)init
{
    self = [super init];
     if (self) {
    _objectsArray = [NSMutableArray array];
    _lock = [[NSLock alloc] init];
 }
  return self;
}

- (void)push:(id)object
{
   [_lock lock];
   [_objectsArray addObject:object];
   [_lock unlock];
}

// Or using the @synchronized construct:

 @synchronized (self) {
    [_elements addObject:element];
  }
@end

he ThreadSafeQueue class above has an init method which initializes two ivars: an _objectsArray array and an NSLock. It has a push: method which acquires the lock, inserts an _object into the array, and then releases the lock. Many threads can call push: at the same time, but the line [_objectsArray addObject:object] will only ever be run on one thread at a time. The steps might go something like this:

 Thread A calls the push: method
 Thread B calls the push: method
 Thread B calls [_lock lock] - since nobody else is holding the lock, 
 Thread B acquires the lock
 Thread A calls [_lock lock] but the lock is held by Thread B so the method call doesn’t return - this pauses execution in thread A
 Thread B adds its objects to _objectsArray and calls [_lock unlock]. When this happens, Thread A’s [_lock lock] method returns and it goes on to insert its own object

We can implement this more succinctly using the @synchronized construct:

The synchronized block has the same effect as the [_lock lock] and [_lock unlock] in the above example. You can think of it as locking on self as if self is an NSLock. A lock is aqcuired before any code after the opening { is run, and the lock is released before any code after the closing } is run. This is really handy because it means that you can never forget to call unlock!

// Or using the @synchronized construct:

 @synchronized (self) {
    [_elements addObject:element];
  }

You can @synchronize on any Objective-C object. So we could just as well have used @synchronized(_elements) instead of @synchronized(self) in the example above and the effect would be the same.

Upvotes: 0

Related Questions