0

I am working with an external device that I receive data from. I want to handle its data read/write queue asynchronously, in a thread.

I've got it mostly working: There is a class that simply manages the two streams, using the NSStreamDelegate to respond to incoming data, as well as responding to NSStreamEventHasSpaceAvailable for sending out data that's waiting in a buffer after having failed to be sent earlier.

This class, let's call it SerialIOStream, does not know about threads or GCD queues. Instead, its user, let's call it DeviceCommunicator, uses a GCD queue in which it initializes the SerialIOStream class (which in turn creates and opens the streams, scheduling them in the current runloop):

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

That way, the SerialIOStreams stream:handleEvent: method runs in that GCD queue, apparently.

However, this causes some problems. I believe I run into concurrency issues, up to getting crashes, mainly at the point of feeding pending data to the output stream. There's a critical part in the code where I pass the buffered output data to the stream, then see how much data was actually accepted into the stream, and then removing that part from my buffer:

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];
    }
}

The above code can get called from two places:

  1. From the user (DeviceCommunicator)
  2. From the local stream:handleEvent: method, after being told that there's space in the output stream.

Those may be (well, surely are) running in separate thread, and therefore I need to make sure they do not run concurrently this code.

I thought I'd solve this by using the following code in DeviceCommunicator when sending new data out:

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

(writeData adds the data to dataToWrite, see above, and then runs the above code that sends it to the stream.)

However, that doesn't work, apparently because ioQueue is a concurrent queue, which may decide to use any available thread, and therefore lead to a race condition when writeData get called by the DeviceCommunicator while there's also a call to it from stream:handleEvent:, on separate threads.

So, I guess I am mixing expectations of threads (which I'm a bit more familiar with) into my apparent misunderstandings with GCD queues.

How do I solve this properly?

I could add an NSLock, protecting the writeData method with it, and I believe that would solve the issue in that place. But I am not so sure that that's how GCD is supposed to be used - I get the impression that'd be a cludge.

Shall I rather make a separate class, using its own serial queue, for accessing and modifying the dataToWrite buffer, perhaps?

I am still trying to grasp the patterns that are involved with this. Somehow, it looks like a classic producer / consumer pattern, but on two levels, and I'm not doing this right.

Thomas Tempelmann
  • 11,045
  • 8
  • 74
  • 149

1 Answers1

2

Long story, short: Don't cross the streams! (haha)

NSStream is a RunLoop-based abstraction (which is to say that it intends to do its work cooperatively on an NSRunLoop, an approach which pre-dates GCD). If you're primarily using GCD to support concurrency in the rest of your code, then NSStream is not an ideal choice for doing I/O. GCD provides its own API for managing I/O. See the section entitled "Managing Dispatch I/O" on this page.

If you want to continue to use NSStream, you can either do so by scheduling your NSStreams on the main thread RunLoop or you can start a dedicated background thread, schedule it on a RunLoop over there, and then marshal your data back and forth between that thread and your GCD queues. (...but don't do that; just bite the bullet and use dispatch_io.)

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • I am given the NSStream objects as I'm doing Bluetooth comms in iOS (`EASession` provides them), so there may be no way to avoid them. I've currently worked around my issue by using `@synchronized (self) { ... }` aaround the critical parts, and that seems to work. Not much better than using NSLocks, though, as I suspect I am causing more thread context switching than necessary. – Thomas Tempelmann Jun 10 '17 at 19:02
  • 1
    @ThomasTempelmann If that works for you, don't let me stop you, but I'm reasonably confident that `NSStream` objects were not intended to be used this way, and there may be thread affinity issues that aren't apparent to you as a consumer (i.e. it could be using thread-local storage behind the scenes). Being NSRunLoop-based is a pretty strong indicator that a class was designed to be used from a single thread, so keep that in mind. – ipmcc Jun 15 '17 at 11:13