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.