我有蓝牙连接配件的输入流和输出流
我想实现以下目标:
将数据写入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在一个单独的线程上运行,它会在触发时设置一个布尔值,然后如果收到数据,还会让handleEvent委托方法设置另一个布尔值。 在该方法中,我们有一个带有睡眠的while循环,当设置其中一个bool时它将停止。
我遇到的问题是在'超时情况'中我的timerFireMethod没有被调用。我的直觉是,我实际上并没有正确地在单独的线程上设置计时器。
有人能看到这里出了什么问题,或者建议更好地实施上述要求吗?
答案 0 :(得分:4)
而是针对固有的异步问题强加不恰当的同步方法,使您的方法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
子类 并不是应该的那么简单。会出现细微的并发问题,迫使您使用锁定或调度队列等同步原语以及其他一些技巧来使其真正可靠。
幸运的是,将任何运行循环任务包装到并发NSOperation
子类中需要基本相同的"锅炉板"码。因此,一旦你有一个通用的解决方案,编码工作只是从模板中复制/过去"然后根据您的特定目的定制代码。
严格来说,如果您不打算将一些任务放入NSOperation
,您甚至不需要NSOperationQueue
的子类。 并发操作可以通过发送start
方法来启动 - 没有NSOperationQueue
必需。然后,不使用NSOperation
的子类可以使您自己的实现更简单,因为子类化NSOperation
本身有其自己的微妙之处。
但是,你实际上需要一个"操作对象"它包装你的Run Loop驱动一个NSStream
对象,因为实现需要保持状态,这在一个简单的异步方法中是无法实现的。
因此,您可以使用任何自定义类,该类可以被视为具有start
和cancel
方法的异步操作,并具有在底层任务完成时通知呼叫站点的机制。
还有更强大的方法来通知呼叫站点而不是完成处理程序。例如:承诺或期货(参见维基文章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;
}
注意:承诺设置了"超时"。这基本上是注册计时器和处理程序。如果计时器在底层任务
[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
方法轻松完成此任务,该方法可以有效地进入运行循环,并等待承诺得到解决:
-(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不会阻塞主线程而不会轮询run循环。当promise被解决时,它将离开运行循环。其他运行循环事件将照常处理。
注意:您可以安全地从主线程中调用testSendingCommandShouldReturnResponseBeforeTimeout10
而不会阻止它。这是绝对必要的,因为您的Stream委托方法也可以在主线程上执行!
在单元测试库中通常可以找到其他方法,它们提供类似于"等待"对于进入运行循环的异步方法或操作的结果。
"等待"的其他方法建议不要使用异步方法或操作的最终结果。这些通常会将方法分派给私有线程,然后阻塞它直到结果可用。
类似于类的操作的代码片段(在Gist上),它使用Promises将流复制到另一个流中: RXStreamToStreamCopier