首页 > 解决方案 > macOS 10.14、Xcode 9:即使使用 @synchronized,tsan 仍然会吠叫

问题描述

在一个用 Objective-C 编写的旧项目中,手动保留/释放,在 NSOperationQueue 的子类中,我有以下代码:

- (NSError*)error {
        NSError* error;
        @synchronized(self) {
                error = m_error;  // Debugger stops here
        }
        return error;
}

- (void)setError:(NSError*)error
  operationGroup:(NSString*)operationGroup {
        error = [error errorByAddingUserInfoObject:operationGroup
                                            forKey:constKeySSYOperationGroup];

        @synchronized(self) {
                [m_error release];
                m_error = error;
                [m_error retain];
        }
}

您会看到它用于@synchronized保护对实例变量 m_error 的访问。尽管如此,当这段代码在 macOS 10.14 Mojave Beta 4 (18A336e) 中运行时,在 Xcode 9.4.1 中,我有时会收到来自 Thread Sanitizer (tsan) 的投诉:

WARNING: ThreadSanitizer: data race (pid=60795)
  Read of size 8 at 0x7b14000cc528 by thread T20:
    #0 -[SSYOperationQueue error] <null> (Bkmxwork:x86_64+0x2b3613)
    #1 -[SSYOperationQueue observeValueForKeyPath:ofObject:change:context:] <null> (Bkmxwork:x86_64+0x2b50d7)
    #2 NSKeyValueNotifyObserver <null> (Foundation:x86_64+0x3b08b)
    #3 _dispatch_client_callout <null> (libdispatch.dylib:x86_64+0x44c0)

  Previous write of size 8 at 0x7b14000cc528 by thread T13:
    #0 -[SSYOperationQueue setError:operationGroup:] <null> (Bkmxwork:x86_64+0x2b382a)
    #1 -[SSYOperationQueue setError:operation:] <null> (Bkmxwork:x86_64+0x2b3a80)
    #2 -[SSYOperationQueue observeValueForKeyPath:ofObject:change:context:] <null> (Bkmxwork:x86_64+0x2b525d)
    #3 NSKeyValueNotifyObserver <null> (Foundation:x86_64+0x3b08b)
    #4 _dispatch_client_callout <null> (libdispatch.dylib:x86_64+0x44c0)

调试器停在 commented 行// Debugger stops here,这表明这两个访问器之间存在某种对 m_error 的竞争。

可能有什么问题?有人告诉我,tsan 从来没有发出过错误的警报,而且直到现在也没有理由怀疑它。使用去年的 Xcode (9) 和今年的 macOS (10.14) 是否有可能让 tsan 感到困惑?或者它是否像往常一样使用新操作系统变得更智能?在我从 macOS 10.13 升级之前,我没有看到这个警告。

标签: objective-cmultithreading

解决方案


感谢 David Philip Oster 向我解释了这个问题:我的 getter 不是原子的。

假设 getter 的 @synchronized 块执行,分配m_errorerror. 但在 getter 返回之前,调用了 setter 并执行其 @synchronized 块,释放 m_error。然后 getter 返回一个已释放且可能无效的 m_error。

解决方案是添加一个保留和自动释放:

- (NSError*)error {
    NSError* error ;
    @synchronized(self) {
        error = [m_error retain];
    }
    return [error autorelease];
   }

有关这个古老主题的更多信息,请参阅Matt Gallagher 于 2009 年发表的这篇博文


推荐阅读