Reputation: 5578
I'm reading the Apple docs around Thread Safety, and it's not completely clear to me what (in practice) really constitutes a class being thread safe. To help understand this better, what would would all need to be done to the following class to make it thread safe (and why)?
#import "UnsafeQueue.h"
@interface UnsafeQueue()
@property (strong, nonatomic) NSMutableArray *data;
@end
@implementation UnsafeQueue
- (id)peek {
return [self.data firstObject];
}
- (NSUInteger)length {
return [self.data count];
}
- (void)enqueue:(id)datum {
[self.data addObject:datum];
}
// other methods omitted...
@end
Would simply creating an ivar NSLock and then lock/unlock surrounding all interactions with the underlying NSMutableArray?
Does the length method, which is just asking for the array's count, need to do this as well?
Upvotes: 13
Views: 8602
Reputation: 299275
The easiest and best way to make a class thread safe is to make it immutable. Then you don't have to deal with any of this. It just works. It really is worth your time to think about whether you need mutability on multiple threads.
But if an immutable class creates significant problems for your design, then generally the best way to implement it is with GCD rather than locks. GCD has much lower overhead and is generally speaking easier to get right.
In this particular case, I'd implement it along these lines (untested, and I've been working in Swift for a while now, so forgive me if I drop a semicolon):
#import "SafeQueue.h"
@interface SafeQueue()
@property (strong, nonatomic) NSMutableArray *data;
@property (strong, nonatomic) dispatch_queue_t dataQueue;
@end
@implementation SafeQueue
- (instancetype)init {
if (self = [super init]) {
_dataQueue = dispatch_queue_create("SafeQueue.data", DISPATCH_QUEUE_CONCURRENT);
}
return self;
}
- (id)peek {
__block id result = nil;
dispatch_sync(self.dataQueue, ^{ result = [self.data firstObject] });
return result;
}
- (NSUInteger)length {
__block NSUInteger result = 0;
dispatch_sync(self.dataQueue, ^{ result = [self.data count] });
return result;
}
- (void)enqueue:(id)datum {
dispatch_barrier_async(self.dataQueue, ^{ [self.data addObject:datum] });
}
// other methods omitted...
@end
Note the use of dispatch_sync
for all readers and dispatch_barrier_async
for all writers. This is how you keep your overhead to a minimum by allowing parallel readers and exclusive writers. And if there is no contention (which is the normal case), dispatch_sync
has much lower overhead than a lock (NSLock
or @synchronized
or even a pthreads lock).
See Migrating Away from Threads for more advice from Apple on how to better deal with concurrency in Cocoa.
Upvotes: 27
Reputation: 437432
Bottom line, the first step to thread-safety is to ensure that you don't have one thread mutating the object (with it in a possibly inconsistent state) while you're trying to access it from another. So any of a variety of synchronization techniques can be useful. See Synchronization section of the Threading Programming Guide for more information on a variety of types of mechanisms. The reader-writer pattern illustrated by Rob Napier is far more efficient than the @synchronized
directive or NSLock
and is discussed in WWDC 2012 video Asynchronous Design Patterns with Blocks, GCD, and XPC.
By the way, the peek
and length
methods have diminished utility in multi-threaded environment. These suggest a dependency that the naive developer might incorrectly infer between these methods and other methods. For example, just because length
is greater than zero doesn't mean that when you subsequently go to retrieve it, that anything will be there.
I would take a hard look at those methods and ask yourself whether they make sense in the multithreaded environment. I know you probably just meant these are arbitrary examples of thread-safety in mutable arrays, but it's indicative of a broader problem I see frequently in "thread-safe" examples I've seen elsewhere on Stack Overflow, where the synchronization mechanism is often at the wrong level to bear any utility.
Upvotes: 6
Reputation: 114828
Thread safety means that the data structure can be accessed and/or modified by multiple threads without becoming corrupt.
One simple approach is to use Objective-C's @synchronized
capability.
In this case, @synchronized(self.data)
around all of your accesses to the array will ensure that only a single thread can access the array at a time.
Even though length
doesn't modify the array, you still need to protect its access because another thread could potentially modify the array -
#import "SafeQueue.h"
@interface SafeQueue()
@property (strong, nonatomic) NSMutableArray *data;
@end
@implementation SafeQueue
- (id)peek {
@synchronized (self.data) {
return [self.data firstObject];
}
}
- (NSUInteger)length {
@synchronized(self.data) {
return [self.data count];
}
}
- (void)enqueue:(id)datum {
@synchronized(self.data) {
[self.data addObject:datum];
}
}
// other methods omitted...
@end
Upvotes: 6