Thomas Tempelmann
Thomas Tempelmann

Reputation: 12043

Asynchronous NSStream I/O with GCD

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.

Upvotes: 0

Views: 636

Answers (1)

ipmcc
ipmcc

Reputation: 29886

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

Upvotes: 2

Related Questions