Mutexes in Objective-C
In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple threads.
#import <Foundation/Foundation.h>
@interface Container : NSObject
@property (nonatomic, strong) NSMutableDictionary *counters;
@property (nonatomic, strong) NSLock *lock;
- (void)incrementForName:(NSString *)name;
@end
@implementation Container
- (instancetype)init {
self = [super init];
if (self) {
_counters = [NSMutableDictionary dictionaryWithDictionary:@{@"a": @0, @"b": @0}];
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)incrementForName:(NSString *)name {
[self.lock lock];
NSNumber *currentValue = self.counters[name];
self.counters[name] = @(currentValue.integerValue + 1);
[self.lock unlock];
}
@end
void doIncrement(Container *container, NSString *name, int n) {
for (int i = 0; i < n; i++) {
[container incrementForName:name];
}
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Container *container = [[Container alloc] init];
dispatch_group_t group = dispatch_group_create();
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_group_async(group, queue, ^{
doIncrement(container, @"a", 10000);
});
dispatch_group_async(group, queue, ^{
doIncrement(container, @"a", 10000);
});
dispatch_group_async(group, queue, ^{
doIncrement(container, @"b", 10000);
});
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
NSLog(@"%@", container.counters);
}
return 0;
}
In this Objective-C version, we define a Container
class that holds a mutable dictionary of counters. We use an NSLock
to synchronize access to the counters.
The incrementForName:
method locks the mutex before accessing the counters, and unlocks it at the end of the method.
In the main
function, we create a Container
instance and use Grand Central Dispatch (GCD) to run several blocks concurrently. Each block calls the doIncrement
function, which increments a named counter in a loop.
We use a dispatch group to wait for all the blocks to finish before printing the final counter values.
To run the program, save it as Mutexes.m
and compile it with:
$ clang -framework Foundation Mutexes.m -o Mutexes
Then run it:
$ ./Mutexes
2023-06-01 12:34:56.789 Mutexes[12345:67890] {
a = 20000;
b = 10000;
}
The output shows that the counters were updated as expected, demonstrating that the mutex successfully protected the shared state from race conditions.
Next, we’ll look at implementing this same state management task using only blocks and dispatch queues.