Stateful Goroutines in Objective-C

Our example demonstrates how to manage state using a dedicated thread and message passing in Objective-C. This approach aligns with the idea of sharing memory by communicating and having each piece of data owned by exactly one thread.

#import <Foundation/Foundation.h>

// Define structs for read and write operations
typedef struct {
    int key;
    void (^response)(int);
} ReadOp;

typedef struct {
    int key;
    int val;
    void (^response)(BOOL);
} WriteOp;

@interface StateManager : NSObject
@property (nonatomic, strong) NSMutableDictionary *state;
@property (nonatomic, strong) dispatch_queue_t queue;
@end

@implementation StateManager

- (instancetype)init {
    self = [super init];
    if (self) {
        _state = [NSMutableDictionary dictionary];
        _queue = dispatch_queue_create("com.example.stateManager", DISPATCH_QUEUE_SERIAL);
    }
    return self;
}

- (void)handleReadOp:(ReadOp)readOp {
    dispatch_async(self.queue, ^{
        int value = [self.state[@(readOp.key)] intValue];
        readOp.response(value);
    });
}

- (void)handleWriteOp:(WriteOp)writeOp {
    dispatch_async(self.queue, ^{
        self.state[@(writeOp.key)] = @(writeOp.val);
        writeOp.response(YES);
    });
}

@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        __block unsigned long long readOps = 0;
        __block unsigned long long writeOps = 0;
        
        StateManager *stateManager = [[StateManager alloc] init];
        
        // Start 100 read operations
        for (int r = 0; r < 100; r++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                for (int i = 0; i < 1000; i++) {
                    ReadOp readOp = {
                        .key = arc4random_uniform(5),
                        .response = ^(int value) {
                            OSAtomicIncrement64(&readOps);
                        }
                    };
                    [stateManager handleReadOp:readOp];
                    [NSThread sleepForTimeInterval:0.001];
                }
            });
        }
        
        // Start 10 write operations
        for (int w = 0; w < 10; w++) {
            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
                for (int i = 0; i < 1000; i++) {
                    WriteOp writeOp = {
                        .key = arc4random_uniform(5),
                        .val = arc4random_uniform(100),
                        .response = ^(BOOL success) {
                            OSAtomicIncrement64(&writeOps);
                        }
                    };
                    [stateManager handleWriteOp:writeOp];
                    [NSThread sleepForTimeInterval:0.001];
                }
            });
        }
        
        // Let the operations work for a second
        [NSThread sleepForTimeInterval:1.0];
        
        // Report the operation counts
        NSLog(@"readOps: %llu", readOps);
        NSLog(@"writeOps: %llu", writeOps);
    }
    return 0;
}

In this Objective-C version, we use Grand Central Dispatch (GCD) to manage concurrency. The StateManager class encapsulates the state and provides methods to handle read and write operations. The state is accessed only on a dedicated serial queue to ensure thread safety.

We start 100 read operation threads and 10 write operation threads, each performing 1000 operations. The operations are executed asynchronously, and we use atomic increments to count the number of completed operations.

To run the program, compile it with the Objective-C compiler and execute the resulting binary:

$ clang -framework Foundation main.m -o stateful_operations
$ ./stateful_operations

Running our program shows that it completes about 80,000 total operations in one second:

readOps: 71708
writeOps: 7177

This approach using a dedicated thread (via GCD’s serial queue) for state management is more involved than using locks, but it can be useful in certain cases. For example, when you have other asynchronous operations involved or when managing multiple locks would be error-prone. You should use whichever approach feels most natural, especially with respect to understanding the correctness of your program.