我有一个通过蓝牙与ExternalAccessory通信的应用,响应有些延迟,因此我希望IO在后台线程上发生。

我为单线程操作设置了NSOperationQueue来排队我的请求:

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

如果我计划从该队列读取和写入EAAccessory流,则我的应用程序将崩溃,因为在队列使用的线程上没有NSRunLoop的情况下,无法传递套接字中的数据。初始化队列后,立即创建一个带有空NSMachPort的运行循环,以使其继续运行并启动它:
[self.sessionQueue addOperationWithBlock:^{
    NSRunLoop* queueLoop = [NSRunLoop currentRunLoop];
    [queueLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    [queueLoop run]; // <- this blocks
}];

由于运行循环永远不会退出,因此会阻塞队列,但是我不确定如何正确管理运行循环,以便可以成功地从附件流中读取数据。

最佳答案

您不应该尝试在NSOperation内运行运行循环。 Grand Central Dispatch拥有在其上运行操​​作的线程。您应该启动自己的线程,并将其运行循环用于 session 流。

However, you need to be aware that NSRunLoop is not generally thread safe, but CFRunLoop is.这意味着要在 session 处理线程上运行块时,您需要降至CFRunLoop级别。

另外,获取对后台线程的运行循环的引用的唯一方法是在该后台线程上运行某些内容。因此,第一步是创建自己的NSThread子类,该子类导出其自己的运行循环:

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

现在,您可以创建MyThread实例,并传入回调。当您启动MyThread时,它将使该回调在主线程上运行,并将其自己的(MyThread的)运行循环传递给该回调。因此,您可以使用MyThread作为 session 处理线程,如下所示:
@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

07-26 09:38
查看更多