3

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.

alfwatt
  • 2,010
  • 2
  • 18
  • 29
  • "If the run loop is being run because you want to get a notification or similar kinds of API commitments, then you need to be careful. You can't just eliminate the running of that loop and have things keep working. But similarly don't just move all the thread code into an NSOperation (with run loop running intact) and throw it in an operation queue; having an NSOperation block running the run loop is not a smart use of NSOperationQueues." -https://lists.apple.com/archives/cocoa-dev/2009/Sep/msg01145.html – alfwatt Apr 01 '16 at 21:25
  • An example: https://horseshoe7.wordpress.com/2015/04/29/nsoperation-and-nsrunloop-a-marriage-of-necessity/ – alfwatt Apr 05 '16 at 18:19

2 Answers2

2

You shouldn't try to run a run loop inside an NSOperation. Grand Central Dispatch owns the thread on which the operation is running. You should start your own thread and use its run loop for your session streams.

However, you need to be aware that NSRunLoop is not generally thread safe, but CFRunLoop is. This means that you need to drop down to the CFRunLoop level when you want to run a block on your session-handling thread.

Also, the only way to get a reference to a background thread's run loop is to run something on that background thread. So step one is to create your own NSThread subclass that exports its own run loop:

typedef void (^MyThreadStartCallback)(CFRunLoopRef runLoop);

@interface MyThread: NSThread

/// After I'm started, I dispatch to the main queue to call `callback`,
// passing my runloop. Then I destroy my reference to `callback`.
- (instancetype)initWithCallback:(MyThreadStartCallback)callback;

@end

@implementation MyThread {
    MyThreadStartCallback _callback;
}

- (instancetype)initWithCallback:(MyThreadStartCallback)callback {
    if (self = [super init]) {
        _callback = callback;
    }
    return self;
}

- (void)main {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    dispatch_async(dispatch_get_main_queue(), ^{
        _callback(runLoop);
    });
    _callback = nil;
    CFRunLoopRun();
}

@end

Now you can create an instance of MyThread, passing in a callback. When you start MyThread, it will make that callback run back on the main thread, and it will pass its own (MyThread's) run loop to the callback. So you can use a MyThread as your session-handling thread, like this:

@implementation Thing {
    CFRunLoopRef _sessionRunLoop;
}

- (void)scheduleStreamsOfSession:(EASession *)session {
    MyThread *thread = [[MyThread alloc] initWithCallback:^(CFRunLoopRef runLoop) {
        // Here I'm on the main thread, but the session-handling thread has
        // started running and its run loop is `runLoop`.
        [self scheduleStreamsOfSession:session inRunLoop:runLoop];
    }];
    [thread start];
}

- (void)scheduleStreamsOfSession:(EASession *)session inRunLoop:(CFRunLoopRef)runLoop {

    // Here I'm on the main thread. I'll save away the session-handling run loop
    // so I can run more blocks on it later, perhaps to queue data for writing
    // to the output stream.
    _sessionRunLoop = runLoop;

    NSInputStream *inputStream = session.inputStream;
    NSOutputStream *outputStream = session.outputStream;

    // Here I'm on the main thread, where it's not safe to use the
    // session-handling thread's NSRunLoop, so I'll send a block to
    // the session-handling thread.
    CFRunLoopPerformBlock(runLoop, kCFRunLoopCommonModes, ^{

        // Here I'm on the session-handling thread, where it's safe to
        // use NSRunLoop to schedule the streams.
        NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
        [inputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];
        [outputStream scheduleInRunLoop:currentRunLoop forMode:NSRunLoopCommonModes];

    });

    // CFRunLoopPerformBlock does **not** wake up the run loop. Since I want
    // to make sure the block runs as soon as possible, I have to wake up the
    // run loop manually:
    CFRunLoopWakeUp(_sessionRunLoop);
}

@end
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • This is the correct answer (and a well written one) for a servicing a NSRunLoop from an NSThread, but I'm asking about NSOperations in serial NSOperationQueues specifically. – alfwatt Apr 05 '16 at 20:22
1

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.

Heath Borders
  • 30,998
  • 16
  • 147
  • 256
alfwatt
  • 2,010
  • 2
  • 18
  • 29