ios – 如何实现超时/等待NSStream以有效地使方法同步

我有一个蓝牙连接附件的输入流和输出流

我想实现以下目标:

将数据写入outputStream
等到inputStream上收到数据或直到10秒后才通过
如果inputStream数据到达返回数据
否则返回零

我尝试这样实现:

- (APDUResponse *)sendCommandAndWaitForResponse:(NSData *)request {
  APDUResponse * result;
  if (!deviceIsBusy && request != Nil) {
    deviceIsBusy = YES;
    timedOut = NO;
    responseReceived = NO;
    if ([[mySes outputStream] hasSpaceAvailable]) {
      [NSThread detachNewThreadSelector:@selector(startTimeout) toTarget:self withObject:nil];
      [[mySes outputStream] write:[request bytes] maxLength:[request length]];
      while (!timedOut && !responseReceived) {
        sleep(2);
        NSLog(@"tick");
      }
      if (responseReceived && response !=nil) {
        result = response;
        response = nil;
      }
      [myTimer invalidate];
      myTimer = nil;
    }
  }
  deviceIsBusy = NO;
  return result;
}

- (void) startTimeout {
  NSLog(@"start Timeout");
  myTimer = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(timerFireMethod:) userInfo:nil repeats:YES];
  [[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSRunLoopCommonModes];
}

- (void)timerFireMethod:(NSTimer *)timer {
  NSLog(@"fired");
  timedOut = YES;
}

- (void)stream:(NSStream*)stream handleEvent:(NSStreamEvent)streamEvent
{
  switch (streamEvent)
  {
    case NSStreamEventHasBytesAvailable:
      // Process the incoming stream data.
      if(stream == [mySes inputStream])
      {
        uint8_t buf[1024];
        unsigned int len = 0;
        len = [[mySes inputStream] read:buf maxLength:1024];
        if(len) {
          _data = [[NSMutableData alloc] init];
          [_data appendBytes:(const void *)buf length:len];
          NSLog(@"Response: %@", [_data description]);
          response = [[APDUResponse alloc] initWithData:_data];
          responseReceived = YES;
        } else {
          NSLog(@"no buffer!");
        }
      }
      break;
     ... //code not relevant 
  }
}

所以理论上是让一个NSTimer在一个单独的线程上运行,当它被触发时会设置一个布尔值,然后如果收到数据,还会让handleEvent委托方法设置另一个布尔值.
在该方法中,我们有一个带有睡眠的while循环,当设置其中一个bool时它将停止.

我遇到的问题是在’超时情况’中我的timerFireMethod没有被调用.我的直觉是我实际上没有正确地在单独的线程上设置计时器.

任何人都可以看到这里出了什么问题或建议更好地实现上述要求吗?

最佳答案
而不是为一个固有的异步问题强加一个不适当的同步方法,使你的方法sendCommandAndWaitForResponse异步.

可以将“流写入”任务包装到异步操作/任务/方法中.例如,您可以使用以下接口以NSOperation的并发子类结束:

typedef void (^DataToStreamCopier_completion_t)(id result);

@interface DataToStreamCopier : NSOperation

- (id) initWithData:(NSData*)sourceData
  destinationStream:(NSOutputStream*)destinationStream
         completion:(DataToStreamCopier_completion_t)completionHandler;

@property (nonatomic) NSThread* workerThread;
@property (nonatomic, copy) NSString* runLoopMode;
@property (atomic, readonly) long long totalBytesCopied;


// NSOperation
- (void) start;
- (void) cancel;
@property (nonatomic, readonly) BOOL isCancelled;
@property (nonatomic, readonly) BOOL isExecuting;
@property (nonatomic, readonly) BOOL isFinished;

@end

您可以使用cancel方法实现“超时”功能.

您的方法sendCommandAndWaitForResponse:与完成处理程序异步:

- (void)sendCommand:(NSData *)request 
         completion:(DataToStreamCopier_completion_t)completionHandler
{
    DataToStreamCopier* op = [DataToStreamCopier initWithData:request 
                                            destinationStream:self.outputStream 
                                                   completion:completionHandler];
   [op start];

   // setup timeout with block:  ^{ [op cancel]; }
   ...
}

用法:

[self sendCommand:request completion:^(id result) {
    if ([result isKindOfClass[NSError error]]) {
        NSLog(@"Error: %@", error);
    }
    else {
        // execute on a certain execution context (main thread) if required:
        dispatch_async(dispatch_get_main_queue(), ^{
            APDUResponse* response = result;
            ...    
        });
    }
}];

警告:

不幸的是,使用运行循环的底层任务正确实现并发NSOperation子类并不是应该的那么简单.会出现细微的并发问题,迫使您使用同步原语(如锁或调度队列)以及其他一些技巧来使其真正可靠.

幸运的是,将任何Run Loop任务包装到并发的NSOperation子类中需要基本相同的“样板”代码.因此,一旦您拥有通用解决方案,编码工作只需从“模板”复制/过去,然后根据您的特定目的定制代码.

替代方案:

严格来说,如果您不打算将大量任务放入NSOperationQueue,则甚至不需要NSOperation的子类.可以简单地开始并发操作,并向其发送start方法 – 不需要NSOperationQueue.然后,不使用NSOperation的子类可以使您自己的实现更简单,因为子类化NSOperation本身有其自己的细微之处.

但是,您实际上需要一个“操作对象”来包装您的Run Loop来驱动NSStream对象,因为实现需要保持状态,这在一个简单的异步方法中是无法实现的.

因此,您可以使用任何自定义类,可以将其视为具有start和cancel方法的异步操作,并具有在底层任务完成时通知调用站点的机制.

除了完成处理程序之外,还有更强大的方法来通知调用站点.例如:承诺或期货(参见wiki文章Futures and promises).

假设您使用Promise实现了自己的“异步操作”类作为通知呼叫站点的方法,例如:

@interface WriteDataToStreamOperation : AsyncOperation

- (void) start;
- (void) cancel;

@property (nonatomic, readonly) BOOL isCancelled;
@property (nonatomic, readonly) BOOL isExecuting;
@property (nonatomic, readonly) BOOL isFinished;
@property (nonatomic, readonly) Promise* promise;

@end

你的原始问题看起来会更“同步” – 尽管sill是异步的:

您的sendCommand方法变为:

注意:假定Promise类的某个实现:

- (Promise*) sendCommand:(NSData *)command {
    WriteDataToStreamOperation* op = 
     [[WriteDataToStreamOperation alloc] initWithData:command 
                                         outputStream:self.outputStream];
    [op start];
    Promise* promise = op.promise;
    [promise setTimeout:100]; // time out after 100 seconds
    return promise;
}

注意:promise已设置“超时”.这基本上是注册计时器和处理程序.如果计时器在底层任务解析了promise之前触发,则计时器块会以超时错误解析promise.如何实现(和IF)取决于Promise库. (这里,我假设是RXPromise库,我是作者.其他实现也可以实现这样的功能).

用法:

[self sendCommand:request].then(^id(APDUResponse* response) {
    // do something with the response
    ...
    return  ...;  // returns the result of the handler
}, 
^id(NSError*error) {
    // A Stream error or a timeout error
    NSLog(@"Error: %@", error);
    return nil;  // returns nothing
});

替代用法:

您可以以不同的方式设置超时.现在,假设我们没有在sendCommand:方法中设置超时.

我们可以在“外部”设置超时:

Promise* promise = [self sendCommand:request];
[promise setTimeout:100];
promise.then(^id(APDUResponse* response) {
    // do something with the response
    ...
    return  ...;  // returns the result of the handler
}, 
^id(NSError*error) {
    // A Stream error or a timeout error
    NSLog(@"Error: %@", error);
    return nil;  // returns nothing
});

使异步方法同步

通常,您不需要也不应该将异步方法“转换”为应用程序代码中的某个同步方法.这总是导致次优和低效的代码,这不必要地消耗系统资源,如线程.

尽管如此,您可能希望在有意义的单元测试中执行此操作:

单元测试中“同步”异步方法的示例

在测试实现时,您经常需要“等待”(同步)以获得结果.您的基础任务实际上是在运行循环上执行的事实,可能在您希望等待结果的同一线程上,这并不会使解决方案更简单.

但是,您可以使用runLoopWait方法轻松地使用RXLromise库来实现这一点,该方法有效地进入运行循环并等待承诺得到解决:

-(void) testSendingCommandShouldReturnResponseBeforeTimeout10 {
    Promise* promise = [self sendCommand:request];
    [promise setTimeout:10];
    [promise.then(^id(APDUResponse* response) {
        // do something with the response
        XCTAssertNotNil(response);            
        return  ...;  // returns the result of the handler
    }, 
    ^id(NSError*error) {
         // A Stream error or a timeout error
        XCTestFail(@"failed with error: %@", error);
        return nil;  // returns nothing

    }) runLoopWait];  // "wait" on the run loop
}

这里,方法runLoopWait将进入运行循环,并等待通过超时错误或基础任务解决了promise时解析promise. promise不会阻塞主线程而不会轮询run循环.当promise被解决时,它将离开运行循环.其他运行循环事件将照常处理.

注意:您可以安全地从主线程调用testSendingCommandShouldReturnResponseBeforeTimeout10而不阻塞它.这是绝对必要的,因为您的Stream委托方法也可以在主线程上执行!

单元测试库中通常还有其他方法,它们提供了一个类似的功能,可以在进入运行循环时“等待”异步方法或操作的结果.

不推荐其他“等待”异步方法或操作的最终结果的方法.这些通常会将方法分派给私有线程,然后阻塞它直到结果可用.

有用的资源

类似于类的操作的代码片段(在Gist上),它使用Promises将流复制到另一个流中:
RXStreamToStreamCopier

转载注明原文:ios – 如何实现超时/等待NSStream以有效地使方法同步 - 代码日志