How to properly open and close a NSStream on another thread
Asked Answered
F

1

9

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.

Firm answered 11/6, 2012 at 13:18 Comment(1)
Did you get any update on this ?Two
O
1

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

Omalley answered 8/7, 2020 at 19:7 Comment(4)
I've never used NSOperation. Where do you put the stream event handling function, and shouldn't you attach it to the stream before you set the stream on the run loop? Does starting an NSOperation mean that it automatically runs on its own thread?Mcduffie
Just implement the stream delegate protocol inside that NSOperation. Returning YES in the -isAsynchronous method guarantees that it's on its own thread. I think it uses Grand Central Dispatch under the hood though. And I think you can actually specify a dispatch queue to run the NSOperationQueue on when you construct it. That way you can ensure it's in a unique thread, if you care that much.Omalley
The threading is really important for communication streams. I have solved the problem in a better way, but this is the second best I've come across. Too many people just cut and paste the same old examples, which are not adequate for real time communication. Thank you for this answer!Mcduffie
Glad you were able to resolve it! Definitely consider NSOperations in the future. They are a fantastic way to encapsulate asynchronous operations and provide a lot of good facility for managing the lifecycle of such operations.Omalley

© 2022 - 2024 — McMap. All rights reserved.