Search code examples
iosmacosconcurrencyfoundationnsrunloop

How do I use an NSRunLoop on an NSOperationQueue?


I have an app which communicates with an ExternalAccessory over Bluetooth, there is some delay in responses so I want the IO to happen on a background thread.

I setup an NSOperationQueue for single-threaded operation to enqueue my requests:

self.sessionQueue = [NSOperationQueue new];
self.sessionQueue.maxConcurrentOperationCount = 1;

If I schedule reads and writes to the EAAccessory streams from that queue, my app crashes because data from the socket can't be delivered without an NSRunLoop on the thread that the queue is using. Immediately after initializing the queue, I create a run loop with an empty NSMachPort to keep it running and start it:

[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop* queueLoop = [NSRunLoop currentRunLoop];
    [queueLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [queueLoop run]; // <- this blocks
}];

This blocks the queue as the run loop will never exit, but I'm not sure how to correctly manage the run loop so that I can successfully read from the accessory streams.


Solution

  • Any thread can have an NSRunLoop created for it if needed, the main thread of any Cocoa or AppKit application has one running by default and any secondary threads must run them programmatically. If you were spawning an NSThread the thread body would be responsible for starting the NSRunLoop but an NSOperationQueue creates it's own thread or threads and dispatches operations to them.

    When using an API which expects an NSRunLoop to deliver events to and from a background thread, either of your own creation, or one that libdispatch has created, you are responsible for making sure the NSRunLoop is run. Typically you will want to run the loop until some condition is met in each of your NSBlockOperation tasks, I wrote a category on NSRunLoop which simplifies this:

    #import <Foundation/Foundation.h>
    
    @interface NSRunLoop (Conditional)
    -(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum;
    @end
    
    #pragma mark -
    
    @implementation NSRunLoop (Conditional)
    
    -(BOOL)runWhileCondition:(BOOL *)condition inMode:(NSString *)mode inIntervals:(NSTimeInterval) quantum {
        BOOL didRun = NO;
        BOOL shouldRun = YES;
        NSPort *dummyPort = [NSMachPort port];
        [self addPort:dummyPort forMode:NSDefaultRunLoopMode];
        while (shouldRun) {
            @autoreleasepool {
                didRun = [self runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:quantum]];
                shouldRun = (didRun ? *condition : NO);
            }
        }
        [self removePort:dummyPort forMode:NSDefaultRunLoopMode];
        return didRun;
    }
    
    @end
    

    With this condition you can schedule an NSBlockOperation which will start the run loop and run until the specified condition is NO:

    __block BOOL streamOperationInProgress = YES;
    [self.sessionQueue addOperationWithBlock:^{
        NSRunLoop *queueLoop = [NSRunLoop currentRunLoop];
        NSStream *someStream = // from somewhere...
        [someStream setDelegate:self];
        [someStream scheduleInRunLoop:queueLoop forMode:NSDefaultRunLoopMode]:
    
        // the delegate implementation of stream:handleEvent:
        // sets streamOperationInProgress = NO;
    
        [queueLoop
            runWhileCondition:&streamOperationInProgress 
            inMode:NSDefaultRunLoopMode 
            inIntervals:0.001];
    }];
    

    The wrinkle in the above example is putting the BOOL someplace that the delegate can set it to NO when the operation is complete.

    Here's a gist of the NSRunLoop+Condition category.