iosmultithreadingnsstream

How to properly open and close a NSStream on another thread


I have an application that connects to a server using NSStream on another thread. The application also closes the connection should the user decide to log out. The problem is that I am never able to successfully close the stream or the thread upon having the user disconnect. Below is my code sample on how I approach creating a thread for my network and trying to close the stream:

+ (NSThread*)networkThread
{
    static NSThread *networkThread = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        networkThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkThreadMain:) object:nil];

        [networkThread start];
    });

    return networkThread;
}

+ (void)networkThreadMain:(id)sender
{
    while (YES)
    {
        @autoreleasepool {
            [[NSRunLoop currentRunLoop] run];
        }
    }
}

- (void)scheduleInThread:(id)sender
{
    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [inputStream open];
}

- (void)closeThread
{    
    [inputStream close];
    [inputStream removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    [inputStream release];
    inputStream = nil;
}

Call made when trying to connect the inputstream:

[self performSelector:@selector(scheduleInThread:) onThread:[[self class] networkThread] withObject:nil waitUntilDone:YES];

Any advice is greatly appreciated.


Solution

  • The way you're mixing static and instance variables is confusing. Are you married to doing it that way? If you put this inside an NSOperation and ran it using an NSOperationQueue I think you'd get much cleaner encapsulation. The operation will manage its own async thread so you don't have to. Also, I highly recommend using ARC if you can.

    A few notes:

    1. Make sure to set the stream's delegate and handle delegate events. You should probably do that inside the operation (make the operation the delegate) and close the stream and finish the operation when necessary.
    2. There may be other failure conditions for the stream besides NSStreamStatusClosed, such as NSStreamStatusNotOpen, etc. You will probably need to add additional handling, which can be done by listening to the delegate methods.
    3. Your code is probably not working right mainly because your while loop runs the runloop forever. You have to have conditions in which to break out. NSOperation gives you some pretty good standardized ways of doing this.
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface AsyncStreamOperation : NSOperation
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    #import "AsyncStreamOperation.h"
    
    @interface AsyncStreamOperation ()
    
    @property (atomic, strong) AsyncStreamOperation *config;
    
    @property (atomic, strong) NSInputStream *stream;
    
    @property (atomic, assign, getter=isExecuting) BOOL executing;
    @property (atomic, assign, getter=isFinished) BOOL finished;
    
    @end
    
    @implementation AsyncStreamOperation
    
    @synthesize executing = _executing;
    @synthesize finished = _finished;
    
    - (instancetype)initWithStream:(NSInputStream *)stream
    {
        self = [super init];
        
        if(self) {
            _stream = stream;
        }
        
        return self;
    }
    
    - (BOOL)isAsynchronous
    {
        return YES;
    }
    
    - (BOOL)isExecuting
    {
        @synchronized (self) {
            return _executing;
        }
    }
    
    - (void)setExecuting:(BOOL)executing
    {
        @synchronized (self) {
            [self willChangeValueForKey:@"isExecuting"];
            _executing = executing;
            [self didChangeValueForKey:@"isExecuting"];
        }
    }
    
    - (BOOL)isFinished
    {
        @synchronized (self) {
            return _finished;
        }
    }
    
    - (void)setFinished:(BOOL)finished
    {
        @synchronized (self) {
            [self willChangeValueForKey:@"isFinished"];
            _finished = finished;
            [self didChangeValueForKey:@"isFinished"];
        }
    }
    
    - (void)start
    {
        // Get runloop
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        
        // Schedule stream
        [self.stream scheduleInRunLoop:runLoop forMode:NSDefaultRunLoopMode];
        [self.stream open];
        
        // Loop until finished
        // NOTE: If -cancel is not called, you need to add your own logic to close the stream so this loop ends and the operation completes
        while(self.executing && !self.finished && !self.cancelled && self.stream.streamStatus != NSStreamStatusClosed) {
            @autoreleasepool {
                // Maximum speed once per second or CPU goes through the roof
                [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1.0]];
            }
        }
    
        self.executing = NO;
        self.finished = YES;
    }
    
    - (void)cancel
    {
        [super cancel];
        
        [self.stream close];
        
        self.executing = NO;
        self.finished = YES;
    }
    
    @end