Reputation: 556
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
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