1

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.

rmaddy
  • 314,917
  • 42
  • 532
  • 579
Chris
  • 556
  • 1
  • 4
  • 18
  • Create a new array from _objectsArray at each time when you want to add or remove object. After that add or remove object on new array. When you finish, assign new array to _objectsArray and do want you need. Did you try it? – trungduc Oct 29 '17 at 03:53
  • No, I haven't tried that, but that won't work. What happens if the new array gets assigned to _objectsArray in addObject while it is enumerating in __threadProc? That would have to be synchronized as well, which is the issue I'm trying to solve here. – Chris Oct 29 '17 at 14:41
  • Before run `enumerateObjectsUsingBlock` create another array from `_objectsArray` and use it instead of `_objectsArray` with `enumerateObjectsUsingBlock` – trungduc Oct 29 '17 at 14:56

1 Answers1

0

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.

BuLB JoBs
  • 841
  • 4
  • 20