如何使用GCD处理NSStream运行循环调度带来的并发问题?

时间:2015-07-09 01:30:49

标签: cocoa-touch concurrency grand-central-dispatch nsstream nsrunloop

我有以下情况,我在其中创建一个GCD调度队列,并在其中我将NSStream安排到当前NSRunLoop,正如其规范中要求它发出委托事件,然后我使用[[NSRunLoop currentRunLoop run]运行该线程的运行循环。

这会产生三种可能的情况:

  1. 创建一个串行队列,在该队列中,通过流发送初始写入消息,并且只有在来自NSStream对象的委托回调时才会发送其他写入消息,因为尝试在不尊重的情况下编写新消息当运行循环运行锁定队列时,此模式(这将是合乎需要的)将失败。

  2. 创建一个并发队列,可以在其中自由地将消息写入流中,因为发送到队列的块将与运行循环的块同时执行。但是,虽然希望使写入消息和运行循环并发运行,但是当然不希望队列中的块同时运行并尝试同时写入流。

  3. 创建两个队列 - 一个负责保持运行循环活动并接收读取流回调,另一个用于向流发送异步写入消息。这似乎是理想的,但似乎NSStream文档明确指出不应该尝试读取/写入其计划的线程之外的流。

  4. 鉴于这些情况都不理想,如何解决这些问题?

2 个答案:

答案 0 :(得分:3)

正如您在文档中所指出的那样,当您拥有基于运行循环的API(如NSStream)时,一般的期望是与该对象的所有交互都将发生在拥有运行循环的线程上。 #39;预定。在使用NSStream时,我不确定混合这两个习语(GCD和运行循环)是否真的任何好处。

除了主队列之外,GCD没有线程关联的概念,所以除非运行循环你调度NSStream恰好是主线程运行循环,否则没有好办法使用dispatch_async来安排在该线程上执行的块。

冒着明显的风险,您应该只使用标准方法来调度其他线程上的方法。 -performSelector:onThread:withObject:waitUntilDone:modes:是最明显的。如果您想要使用块的困惑,那么有必要知道堆分配的块可以像Objective-C对象一样对待,并像-invoke那样实现NSInvocation选择器。与您的问题相关的一个简单示例可能如下所示:

@interface AppDelegate ()
{
    NSThread* bgthread;
}
@end

@implementation AppDelegate

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    // Basic loop to get the background thread to run until you call -cancel on it
    dispatch_block_t threadMain = [^{
        NSThread* thread = [NSThread currentThread];
        NSParameterAssert(![thread isMainThread]);

        NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop];
        NSPort* port = [NSPort port];

        // If we dont register a mach port with the run loop, it will just exit immediately
        [currentRunLoop addPort: port forMode: NSRunLoopCommonModes];

        // Loop until the thread is cancelled.
        while (!thread.cancelled)
        {
            [currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]];
        }

        [currentRunLoop removePort: port forMode: NSRunLoopCommonModes];

        [port invalidate];
        port = nil;
    } copy];

    // Start the thread
    bgthread = [[NSThread alloc] initWithTarget: threadMain selector: @selector(invoke) object: nil];
    [bgthread start];

    // Fetch the runloop, so you can schedule an NSStream on it...
    __block NSRunLoop* runloopForStream = nil;
    dispatch_block_t getrunloop = [^{
        runloopForStream = [NSRunLoop currentRunLoop];
    } copy];

    // Dispatch synchronously, so that runloopForStream is populated before we continue...
    [getrunloop performSelector: @selector(invoke) onThread: bgthread withObject: nil waitUntilDone: YES];

    // Schedule your stream, etc.
    NSOutputStream* mystream = ...; // Your code here...
    [mystream scheduleInRunLoop: runloopForStream forMode: NSDefaultRunLoopMode];

    // Then later, when you want to write some data...
    NSData* dataToWrite = [NSMutableData dataWithLength: 100];
    dispatch_block_t doWrite = [^{
        [mystream write: dataToWrite.bytes maxLength: dataToWrite.length];
    } copy];

    // Dispatch asynchronously to thread
    [doWrite performSelector: @selector(invoke) onThread: bgthread withObject: nil waitUntilDone: NO];
}
@end

请注意,块的-copy是将它们复制到堆中所必需的,否则当声明方法超出范围时,它们将被释放。

答案 1 :(得分:3)

晚会,但您可以使用

直接为您的流设置所需的调度队列,而不是使用runloops
void CFReadStreamSetDispatchQueue(CFReadStreamRef stream, dispatch_queue_t q);
void CFWriteStreamSetDispatchQueue(CFWriteStreamRef stream, dispatch_queue_t q);

CFReadStreamRef可以将桥接的NSInputStream和CFWriteStreamRef作为桥接的NSOutputStream。这样您就不必安排或取消安排runloops,您的流将在后台运行。

Apple sample code

的摘录
CFReadStreamSetDispatchQueue((__bridge CFReadStreamRef) self.inputStream,  self.queue);
CFWriteStreamSetDispatchQueue((__bridge CFWriteStreamRef) self.outputStream, self.queue);

在Swift中,您可以直接调用函数:

CFReadStreamSetDispatchQueue(inputStream, streamQueue)
CFWriteStreamSetDispatchQueue(outputStream, streamQueue)