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.
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.