【问题标题】:Asynchronous NSStream I/O with GCD带 GCD 的异步 NSStream I/O
【发布时间】:2017-11-11 22:55:11
【问题描述】:

我正在使用从其接收数据的外部设备。我想在一个线程中异步处理它的数据读/写队列。

我已经让它大部分工作了:有一个类可以简单地管理两个流,使用 NSStreamDelegate 响应传入数据,以及响应 NSStreamEventHasSpaceAvailable 发送在缓冲区中等待的数据未能提前发送。

这个类,我们称之为SerialIOStream,不知道线程或GCD队列。相反,它的用户,我们称之为DeviceCommunicator,使用一个 GCD 队列,它在其中初始化 SerialIOStream 类(依次创建和打开流,在当前运行循环中调度它们):

ioQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0);
dispatch_async(ioQueue, ^{ 
    ioStreams = [[SerialIOStream alloc] initWithPath:[@"/dev/tty.mydevice"]];
    [[NSRunLoop currentRunLoop] run];
});

这样,SerialIOStreams stream:handleEvent: 方法显然会在 GCD 队列中运行。

但是,这会导致一些问题。我相信我遇到了并发问题,直到崩溃,主要是在将未决数据提供给输出流的时候。代码中有一个关键部分,我将缓冲的输出数据传递给流,然后查看流中实际接受了多少数据,然后从缓冲区中删除该部分:

NSInteger n = self.dataToWrite.length;
if (n > 0 && stream.hasSpaceAvailable) {
    NSInteger bytesWritten = [stream write:self.dataToWrite.bytes maxLength:n];
    if (bytesWritten > 0) {
        [self.dataToWrite replaceBytesInRange:NSMakeRange(0, bytesWritten) withBytes:NULL length:0];
    }
}

上面的代码可以从两个地方调用:

  1. 来自用户 (DeviceCommunicator)
  2. 来自本地stream:handleEvent: 方法,在被告知输出流中有空间之后。

那些可能(当然是)在单独的线程中运行,因此我需要确保它们不会同时运行此代码。

我想在发送新数据时使用DeviceCommunicator 中的以下代码来解决这个问题:

dispatch_async (ioQueue, ^{
    [ioStreams writeData:data];
});

writeData 将数据添加到dataToWrite,见上文,然后运行将其发送到流的上述代码。)

但是,这不起作用,显然是因为 ioQueue 是一个并发队列,它可能决定使用任何可用的线程,因此当 writeDataDeviceCommunicator 调用时会导致竞争条件,同时还有一个从 stream:handleEvent: 在单独的线程上调用它。

所以,我想我是在对 GCD 队列的明显误解中混入对线程的期望(我比较熟悉)。

我该如何正确解决这个问题?

我可以添加一个 NSLock,用它来保护 writeData 方法,我相信这会解决那个地方的问题。但我不太确定 GCD 应该是如何使用的 - 我觉得这将是一个障碍。

我是否应该创建一个单独的类,使用它自己的串行队列来访问和修改dataToWrite 缓冲区,也许?

我仍在尝试掌握与此相关的模式。不知何故,它看起来像一个经典的生产者/消费者模式,但在两个层面上,我做的不对。

【问题讨论】:

标签: ios grand-central-dispatch nsstream


【解决方案1】:

长话短说:不要越过溪流! (哈哈)

NSStream 是一个基于 RunLoop 的抽象(也就是说,它打算在 NSRunLoop 上协同工作,这是一种早于 GCD 的方法)。如果您主要使用 GCD 在其余代码中支持并发,那么NSStream 不是执行 I/O 的理想选择。 GCD 提供了自己的 API 来管理 I/O。请参阅this page 上标题为“管理调度 I/O”的部分。

如果您想继续使用NSStream,您可以通过将NSStreams 安排在主线程RunLoop 上来实现,或者您可以启动一个专用的后台线程,将其安排在那边的RunLoop 上,然后在该线程和 GCD 队列之间来回编组数据。 (...但不要那样做;只要硬着头皮使用dispatch_io。)

【讨论】:

  • 当我在 iOS 中进行蓝牙通信时,我得到了 NSStream 对象(EASession 提供了它们),因此可能无法避免它们。我目前已经通过在关键部分周围使用@synchronized (self) { ... } 来解决我的问题,这似乎有效。不过,这并不比使用 NSLocks 好多少,因为我怀疑我会导致不必要的线程上下文切换。
  • @ThomasTempelmann 如果这对你有用,不要让我阻止你,但我有理由相信 NSStream 对象不打算以这种方式使用,并且可能存在线程关联作为消费者,您不明显的问题(即它可能在幕后使用线程本地存储)。基于 NSRunLoop 是一个非常强的指标,表明一个类被设计为在单个线程中使用,所以请记住这一点。
猜你喜欢
  • 2013-06-19
  • 1970-01-01
  • 2013-07-15
  • 2011-03-14
  • 1970-01-01
  • 2012-09-14
  • 2015-08-22
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多