【问题标题】:Objective C: Better way to wait on thread completion?目标 C:等待线程完成的更好方法?
【发布时间】:2013-08-10 17:14:11
【问题描述】:

在我的应用程序中,我在每个芯片的多个管道上的多个芯片上执行大量异步 I/O。

一些操作由多个操作组成。例如,要从其中一个芯片读取序列号,我必须执行两次写入和两次读取:写入命令检查序列号缓冲区大小,读取结果。写命令读取序列号值,读取结果。

这是该操作的简化代码:

- (BOOL)readSerialNumber:(NSMutableString*)serialNumber  
{         
  if(nil == serialNumber)  
    return FALSE;  //  Nowhere to store.  

  if(![self sendCommand:GetSerialNumberSize])  
    return FALSE;  

  // Set up some matching event data (not shown), then check it....  

  BOOL matched= FALSE;  
  BOOL timeUp= FALSE;  
  [self setEventWasMatched:FALSE];  
  NSDate* timeUpDate= [[NSDate alloc] initWithTimeIntervalSinceNow:2.0];  
  while((!timeUp) && !matched)  
  {  
   matched= [self eventWasMatched];  // Match state is set from receive code in another thread.  
   timeUp= (NSOrderedDescending != [timeUpDate compare:[NSDate date]]);  
  }  
  [timeUpDate release];  

  NSUInteger serialNumberSize= matchedEvent.serialNumberSize;  
  if(0 == serialNumberSize)  
    return FALSE;  

  if(![self sendCommand:GetSerialNumber ofSize:serialNumberSize])  
    return FALSE;  

  // Set up some matching event data (not shown), then check it....  

  matched= FALSE;  
  timeUp= FALSE;  
  [self setEventWasMatched:FALSE];  
  timeUpDate= [[NSDate alloc] initWithTimeIntervalSinceNow:2.0];  
  while((!timeUp) && !matched)  
  {  
   matched= [self eventWasMatched];  // Match state is set from receive code in another thread.  
   timeUp= (NSOrderedDescending != [timeUpDate compare:[NSDate date]]);  
  }  
  [timeUpDate release];  

  [serialNumber.setString:matchedEvent.serialNumber];  

  return (0 != [serialNumber length]);  
}  

- (void)setEventWasMatched:(BOOL)matched  
{  
  [lockMatch lock];  
  eventMatched= matched;  
  [lockMatch unlock];  
}  

- (void) eventWasMatched  
{  
  BOOL wasMatched= FALSE;
  [lockMatch lock];  
  wasMatched= eventMatched;  
  [lockMatch unlock];  

  return wasMatched;  
}  

此代码示例可能无法编译或工作,但它是我正在工作的代码的一个不错的表示。

这里有几件事我有疑问:

  • 我一直认为 BOOL 的 set/get 函数中的 NSLock 访问是昂贵的,就像在 setEventWasMatched 和 eventWasMatched 中一样。 SO question 10094361 参考了一些分析,Apple Thread Programming Guide 指出“虽然锁是同步两个线程的有效方法,但获取锁是一项相对昂贵的操作,即使在无争议的情况下也是如此。”我怎样才能以更有效的方式做到这一点?

  • 我从 Allocations 工具中知道,在检查事件匹配的循环中使用了大量内存来创建 NSDate 对象。我不能对匹配进行开放式检查,因为我的匹配标准可能永远不会得到满足。有什么更好的方法?

任何意见将不胜感激。有人可能会说使用 NSOperation/NSOperationQueue 或 GCD,但相信我,这个示例是简单操作之一。还有一些涉及多个读/写对,一些写/多个读等。

【问题讨论】:

    标签: objective-c multithreading thread-safety


    【解决方案1】:

    你问:

    ... Apple 线程编程指南指出“虽然锁是同步两个线程的有效方法,但获取锁是一项相对昂贵的操作,即使在无争议的情况下也是如此。”我怎样才能以更有效的方式做到这一点?

    这不是您的 sn-p 效率低下的根源,但如果您想知道替代方案,重构代码以使用队列有时会有所帮助。请参阅 并发编程指南中的 Eliminating Lock Based Code,其中说:

    将基于锁的代码替换为队列可以消除许多与锁相关的惩罚,还可以简化剩余代码。您可以创建一个队列来序列化访问该资源的任务,而不是使用锁来保护共享资源。队列不会施加与锁相同的惩罚。例如,排队任务不需要进入内核来获取互斥锁。

    还有一个特别巧妙的 GCD 解决方案,用于使用读写器模式控制访问(使用并发队列,dispatch_sync 读取,dispatch_barrier_async 写入;这允许并发读取,但相对于写入将它们序列化,这是异步完成的)。坦率地说,这种模式在这里可能并不适用,但在以下视频中进行了讨论:WWDC 2011 - Mastering GCDWWDC 2012 - Asynchronous Design Patterns

    但是,如果您正在寻找效率,那么锁并不是这里的罪魁祸首。这就是while 循环。通过使用调度信号量或类似的东西来解决这个问题,例如,在发送请求后,使用:

    if(![self sendCommand:GetSerialNumberSize])  
        return FALSE;  
    
    self.semaphore = dispatch_semaphore_create(0);
    dispatch_time_t timeout = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    dispatch_semaphore_wait(self.semaphore, timeout);
    
    if (!self.eventWasMatched) {
        // we must have timed out
    }
    
    // carry on ...
    

    ReadPipeAsync 例程可以:

    obj.eventWasMatched = YES;                // update all the other relevant properties, too
    dispatch_semaphore_signal(obj.semaphore); // now inform other process that we had a match
    

    这样的事情可能会更有效率。

    你继续问:

    我从 Allocations 工具中知道,大量内存用于在我的循环中创建 NSDate 对象以检查事件匹配。我不能对匹配进行开放式检查,因为我的匹配标准可能永远不会得到满足。有什么更好的方法?

    是的,您的代码会创建大量 autorelease 对象,这些对象在池耗尽之前不会被释放。显然,您应该完全删除这些 while 循环,但是,为了争论,如果您想解决这里的内存问题,您可以将其包装在 @autoreleasepool 中,这样您就可以用更高的频率:

    NSDate* timeUpDate= [[NSDate alloc] initWithTimeIntervalSinceNow:2.0];  
    while((!timeUp) && !matched)  
    {
        @autoreleasepool {   
            matched = [self eventWasMatched];  // Match state is set from receive code in another thread.   
            timeUp  = (NSOrderedDescending != [timeUpDate compare:[NSDate date]]);  
        }
    }  
    [timeUpDate release];  
    

    或者,更好的是,根本不使用自动释放对象:

    CFAbsoluteTime startTime = CFAbsoluteTimeGetCurrent();
    while((!timeUp) && !matched)
    {
        matched = [self eventWasMatched];  // Match state is set from receive code in another thread.
        timeUp  = ((CFAbsoluteTimeGetCurrent() - startTime) >= 2.0);
    }
    

    但是,正如我之前所说,这确实是一个没有实际意义的问题,因为您应该将这些 while 循环替换为更有效的进程间通信,例如调度信号量。

    【讨论】:

    • Rob - 我喜欢这些建议。该信号量正是我一直在寻找的。我应该阅读更多关于同步对象的内容!
    • 最初设计应用程序时,我阅读了很多关于线程和替代方案的信息。你提到的关于消除基于锁的代码的讨论看起来很有趣。我想我在几个月前掩盖了它。我正在开发的应用程序是从单个芯片开始的,但现在必须同时支持多个芯片,所以当时没有问题的事情开始产生影响!感谢您的建议。
    【解决方案2】:

    我知道 BOOL 的 set/get 函数中的 NSLock 访问很昂贵。

    你怎么知道的?请记住,您正在读取和写入 IO 设备。相比之下,锁将是微不足道的(除非您的 IO 通道与内存总线的速度相似)。

    我知道带有超时检查的手工“睡眠”可能很昂贵

    我没有看到任何睡眠,只有几个紧密的循环。它们在运行时肯定会占用一个 CPU 内核的 100%。

    很难说如何改进这一点,你并没有真正展示足够的代码。你可以用 NSOperations 来做,例如您将有一个写入操作和每个所需读取的操作,并且您将使用依赖项来确保它们以正确的顺序执行,但是,假设读取和写入设备是通过读取和写入文件句柄来完成的,我会使用运行循环和 NSFileHandles。

    【讨论】:

    • 对 - 这是一个简单操作的理想化示例。我对 NSLock 费用的假设是基于我在各种帖子中阅读的内容。我现在在应用程序上运行时间探查器,发现应用程序的大部分时间都花在了从我的数据收集双端队列中删除事件上。我对使用 NSDate 的循环的担忧是,我从分配工具中看到 NSDate 对象在我的内存使用中占很大比例。我的一些测试人员提到 OS X 抱怨我的应用程序使用高内存,尽管这是在启用调试代码的情况下。
    • 应用程序的目标是在芯片上建立一个紧密的读取循环。我在工作线程中使用 ReadPipeAsync。它发布读取,回调捕获结果,将其存储在受 NSLock 保护的双端队列中,并在该线程上发布另一个读取。我在芯片缓冲区溢出方面遇到了一些问题,但这可能是由于代码中其他地方的问题。但是,以防万一,我正在尝试加快速度。如前所述,大量时间用于从该双端队列中提取数据(在处理线程中完成)。我担心双端队列的争用可能会阻止我的读取。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-08-07
    • 2010-09-20
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多