草庐IT

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

coder 2023-09-27 原文

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

我要实现以下目标:

将数据写入outputStream
等待,直到在inputStream上接收到数据,或者直到10秒钟过去
如果inputStream数据到达,则返回数据
否则返回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在单独的线程上运行,该线程将在触发时设置一个 bool 值,然后如果接收到数据,则使handleEvent委托(delegate)方法设置另一个 bool 值。
在该方法中,我们有一个带有 sleep 的while循环,当设置了其中一个 bool 值时,该循环将停止。

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

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

最佳答案

而是对固有的异步问题强加不合适的同步方法,而应使方法sendCommandAndWaitForResponse异步。

可以将“流写入”任务包装到异步操作/任务/方法中。例如,您可以使用以下接口(interface)以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子类中,基本上需要相同的“样板”代码。因此,一旦有了通用解决方案,编码工作就是从"template"进行复制/粘贴,然后为特定目的定制代码。

替代解决方案:

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

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

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

与完成处理程序相比,还有更强大的通知调用站点的方法。例如:promise或future(请参阅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 已设置“超时”。这基本上是在注册计时器和处理程序。如果计时器在底层任务解决了 promise 之前触发了计时器,则计时器块将解决带有超时错误的 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
});

使异步方法同步

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

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

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

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

但是,您可以使用runLoopWait方法,通过RXPromise库轻松完成此操作,该方法有效地进入运行循环,并在该循环中等待可解决的 promise :
-(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不会阻塞主线程,也不会轮询运行循环。解决 promise 后,它将仅离开运行循环。其他运行循环事件将照常处理。

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

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

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

有用的资源

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

关于ios - 如何实现超时/等待NSStream有效地使方法同步,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20700326/

有关ios - 如何实现超时/等待NSStream有效地使方法同步的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  5. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  6. ruby-on-rails - 如何验证 update_all 是否实际在 Rails 中更新 - 2

    给定这段代码defcreate@upgrades=User.update_all(["role=?","upgraded"],:id=>params[:upgrade])redirect_toadmin_upgrades_path,:notice=>"Successfullyupgradeduser."end我如何在该操作中实际验证它们是否已保存或未重定向到适当的页面和消息? 最佳答案 在Rails3中,update_all不返回任何有意义的信息,除了已更新的记录数(这可能取决于您的DBMS是否返回该信息)。http://ar.ru

  7. ruby-on-rails - 'compass watch' 是如何工作的/它是如何与 rails 一起使用的 - 2

    我在我的项目目录中完成了compasscreate.和compassinitrails。几个问题:我已将我的.sass文件放在public/stylesheets中。这是放置它们的正确位置吗?当我运行compasswatch时,它不会自动编译这些.sass文件。我必须手动指定文件:compasswatchpublic/stylesheets/myfile.sass等。如何让它自动运行?文件ie.css、print.css和screen.css已放在stylesheets/compiled。如何在编译后不让它们重新出现的情况下删除它们?我自己编译的.sass文件编译成compiled/t

  8. ruby - 如何将脚本文件的末尾读取为数据文件(Perl 或任何其他语言) - 2

    我正在寻找执行以下操作的正确语法(在Perl、Shell或Ruby中):#variabletoaccessthedatalinesappendedasafileEND_OF_SCRIPT_MARKERrawdatastartshereanditcontinues. 最佳答案 Perl用__DATA__做这个:#!/usr/bin/perlusestrict;usewarnings;while(){print;}__DATA__Texttoprintgoeshere 关于ruby-如何将脚

  9. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  10. ruby - 如何指定 Rack 处理程序 - 2

    Rackup通过Rack的默认处理程序成功运行任何Rack应用程序。例如:classRackAppdefcall(environment)['200',{'Content-Type'=>'text/html'},["Helloworld"]]endendrunRackApp.new但是当最后一行更改为使用Rack的内置CGI处理程序时,rackup给出“NoMethodErrorat/undefinedmethod`call'fornil:NilClass”:Rack::Handler::CGI.runRackApp.newRack的其他内置处理程序也提出了同样的反对意见。例如Rack

随机推荐