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

 纽约纽约MrWaNg 发布于 2023-02-09 01:17

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

我要实现以下目标:

将数据写入outputStream等待,直到在inputStream上接收到数据为止;或者,如果inputStream数据到达则经过10秒钟才返回数据,否则返回nil

我试图这样实现:

- (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循环,当设置了这些布尔之一时,它将停止。

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

谁能看到这里出了什么问题,或者为上述要求提出更好的实施方案?

1 个回答
  • 而是对固有的异步问题强加不合适的同步方法,而应使您的方法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子类中都需要基本相同的“样板”代码。因此,一旦有了通用的解决方案,编码工作就是从“模板”进行复制/粘贴,然后为您的特定目的定制代码。

    替代解决方案:

    严格来说,NSOperation如果您不打算将许多此类任务放入,则您甚至不需要的子类NSOperationQueue。一个并行操作可以简单地开始发送它的start方法-有没有NSOperationQueue 需要。然后,不使用的子类NSOperation会使您自己的实现更简单,因为子类NSOperation本身具有其自身的微妙之处。

    但是,实际上,您需要一个“操作对象”来包装运行对象的Run Loop NSStream,因为实现需要保持状态,而这是无法通过简单的异步方法完成的

    因此,您可以使用任何可被视为异步操作的自定义类,该类具有startcancel方法,并且具有在基础任务完成时通知调用站点的机制。

    与完成处理程序相比,通知呼叫站点的功能也更强大。例如:promise或futures(请参阅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
    

    您的原始问题看起来会更加“同步”-尽管仍然是异步的:

    您的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库。(这里,我假设我是作者的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
    });
    

    使异步方法同步

    通常,您不需要,也不应该在应用程序代码中将异步方法“转换”为某些同步方法。这总是导致次优且效率低下的代码,从而不必要地消耗了系统资源,例如线程。

    但是,您可能需要在有意义的单元测试中执行此操作:

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

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

    但是,您可以使用RXPromise库轻松地完成此任务,runLoopWait方法是有效地进入运行循环并在那儿等待解决诺言的方法:

    -(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不会阻塞主线程,也不会轮询运行循环。解决承诺后,它将仅离开运行循环。其他运行循环事件将照常处理。

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

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

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

    有用的资源

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

    2023-02-09 01:58 回答
撰写答案
今天,你开发时遇到什么问题呢?
立即提问
热门标签
PHP1.CN | 中国最专业的PHP中文社区 | PNG素材下载 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有