GCD,NSThread和performSelector:onThread:问题

我试图debugging一些iOS崩溃日志,其中包含以下错误信息:

***终止应用程序由于未捕获的exception“NSDestinationInvalidException”,原因:'*** – [SomeClass performSelector:onThread:withObject:waitUntilDone:modes:]:目标线程退出时,等待执行

代码的相关部分是:

- (void) runInvocationOnMyThread:(NSInvocation*)invocation { NSThread* currentThread = [NSThread currentThread]; if (currentThread != myThread) { //call over to the correct thread [self performSelector:@selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES]; } else { //we're okay to invoke the target now [invocation invoke]; } } 

这是类似于这里讨论的问题,除了我不想取消我的onThread:线程。 事实上,在我的情况下, onThread:被传递给应用程序主线程的引用,所以它不应该被终止,除非整个应用程序正在终止。

所以第一个问题是,错误消息中引用的“目标”线程是我传递给onThread: ,还是等待onThread:线程完成调用的线程?

我认为这是第二个选项,就好像主线程真的已经终止后台线程的崩溃是有点模糊无论如何。

考虑到这一点,并基于以下来自performSelector:onThread:...的参考文档的讨论performSelector:onThread:...

特别注意事项

该方法向其当前上下文的runloop注册,并依赖于定期运行的runloop来正确执行。 一个可以调用这个方法并最终注册到一个不会定期自动运行的runloop的通用上下文是由调度队列调用的。 如果在调度队列上运行时需要这种types的function,则应使用dispatch_after和相关方法来获取所需的行为。

…我已经修改我的代码, performSelector:onThread:...使用GCD over performSelector:onThread:... ,如下所示:

 - (void) runInvocationOnMyThread:(NSInvocation*)invocation { NSThread* currentThread = [NSThread currentThread]; if (currentThread != myThread) { //call over to the correct thread if ([myThread isMainThread]) { dispatch_sync(dispatch_get_main_queue(), ^{ [invocation invoke]; }); } else { [self performSelector:@selector(runInvocationOnMyThread:) onThread:myThread withObject:invocation waitUntilDone:YES]; } } else { //we're okay to invoke the target now [invocation invoke]; } } 

这似乎工作正常(虽然不知道是否修复了崩溃,因为这是一个非常罕见的崩溃)。 也许有人可以评论这种方法是否比原来更容易崩溃?

无论如何,主要的问题是当目标线程是主线程时,使用GCD只有一个明显的方法。 在我的情况下,这是真的,但我希望能够使用GCD无论是否目标线程是主线程。

所以更重要的问题是,有没有办法将任意NSThread到GCD中相应的队列? 理想情况下, dispatch_queue_t dispatch_get_queue_for_thread(NSThread* thread) ,以便我可以修改我的代码是:

 - (void) runInvocationOnMyThread:(NSInvocation*)invocation { NSThread* currentThread = [NSThread currentThread]; if (currentThread != myThread) { //call over to the correct thread dispatch_sync(dispatch_get_queue_for_thread(myThread), ^{ [invocation invoke]; }); } else { //we're okay to invoke the target now [invocation invoke]; } } 

这是可能的,还是没有从NSThread直接映射到GCD队列,可以应用?

考虑到你要包装一个需要线程相关性的第三方API的目标,你可以尝试使用一个转发代理来确保只在正确的线程上调用方法。 有这样做的一些技巧,但我设法掀起了一些可能的帮助。

假设您有一个对象XXThreadSensitiveObject ,其接口如下所示:

 @interface XXThreadSensitiveObject : NSObject - (instancetype)init NS_DESIGNATED_INITIALIZER; - (void)foo; - (void)bar; - (NSInteger)addX: (NSInteger)x Y: (NSInteger)y; @end 

目标是-foo-bar-addX:Y:始终在同一个线程上调用。

我们还要说,如果我们在主线程上创build这个对象,那么我们的期望就是主线程是有福的线程,所有的调用都应该在主线程上,但是如果它是从任何非主线程创build的,应该产生自己的线程,这样可以保证线程关联前进。 (因为GCD托pipe线程是短暂的,所以没有办法与GCD托pipe线程build立线程关联。

一个可能的实现可能是这样的:

 // Since NSThread appears to retain the target for the thread "main" method, we need to make it separate from either our proxy // or the object itself. @interface XXThreadMain : NSObject @end // This is a proxy that will ensure that all invocations happen on the correct thread. @interface XXThreadAffinityProxy : NSProxy { @public NSThread* mThread; id mTarget; XXThreadMain* mThreadMain; } @end @implementation XXThreadSensitiveObject { // We don't actually *need* this ivar, and we're skankily stealing it from the proxy in order to have it. // It's really just a diagnostic so we can assert that we're on the right thread in method calls. __unsafe_unretained NSThread* mThread; } - (instancetype)init { if (self = [super init]) { // Create a proxy for us (that will retain us) XXThreadAffinityProxy* proxy = [[XXThreadAffinityProxy alloc] initWithTarget: self]; // Steal a ref to the thread from it (as mentioned above, this is not required.) mThread = proxy->mThread; // Replace self with the proxy. self = (id)proxy; } // Return the proxy. return self; } - (void)foo { NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread])); NSLog(@"-foo called on %@", [NSThread currentThread]); } - (void)bar { NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread])); NSLog(@"-bar called on %@", [NSThread currentThread]); } - (NSInteger)addX: (NSInteger)x Y: (NSInteger)y { NSParameterAssert([NSThread currentThread] == mThread || (!mThread && [NSThread isMainThread])); NSLog(@"-addX:Y: called on %@", [NSThread currentThread]); return x + y; } @end @implementation XXThreadMain { NSPort* mPort; } - (void)dealloc { [mPort invalidate]; } // The main routine for the thread. Just spins a runloop for as long as the thread isnt cancelled. - (void)p_threadMain: (id)obj { NSThread* thread = [NSThread currentThread]; NSParameterAssert(![thread isMainThread]); NSRunLoop* currentRunLoop = [NSRunLoop currentRunLoop]; mPort = [NSPort port]; // If we dont register a mach port with the run loop, it will just exit immediately [currentRunLoop addPort: mPort forMode: NSRunLoopCommonModes]; // Just loop until the thread is cancelled. while (!thread.cancelled) { [currentRunLoop runMode: NSDefaultRunLoopMode beforeDate: [NSDate distantFuture]]; } [currentRunLoop removePort: mPort forMode: NSRunLoopCommonModes]; [mPort invalidate]; mPort = nil; } - (void)p_wakeForThreadCancel { // Just causes the runloop to spin so that the loop in p_threadMain can notice that the thread has been cancelled. } @end @implementation XXThreadAffinityProxy - (instancetype)initWithTarget: (id)target { mTarget = target; mThreadMain = [[XXThreadMain alloc] init]; // We'll assume, from now on, that if mThread is nil, we were on the main thread. if (![NSThread isMainThread]) { mThread = [[NSThread alloc] initWithTarget: mThreadMain selector: @selector(p_threadMain:) object:nil]; [mThread start]; } return self; } - (void)dealloc { if (mThread && mThreadMain) { [mThread cancel]; const BOOL isCurrent = [mThread isEqual: [NSThread currentThread]]; if (!isCurrent && !mThread.finished) { // Wake it up. [mThreadMain performSelector: @selector(p_wakeForThreadCancel) onThread:mThread withObject: nil waitUntilDone: YES modes: @[NSRunLoopCommonModes]]; } } mThreadMain = nil; mThread = nil; } - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector { NSMethodSignature *sig = [[mTarget class] instanceMethodSignatureForSelector:selector]; if (!sig) { sig = [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; } return sig; } - (void)forwardInvocation:(NSInvocation*)invocation { if ([mTarget respondsToSelector: [invocation selector]]) { if ((!mThread && [NSThread isMainThread]) || (mThread && [mThread isEqual: [NSThread currentThread]])) { [invocation invokeWithTarget: mTarget]; } else if (mThread) { [invocation performSelector: @selector(invokeWithTarget:) onThread: mThread withObject: mTarget waitUntilDone: YES modes: @[ NSRunLoopCommonModes ]]; } else { [invocation performSelectorOnMainThread: @selector(invokeWithTarget:) withObject: mTarget waitUntilDone: YES]; } } else { [mTarget doesNotRecognizeSelector: invocation.selector]; } } @end 

这里的顺序有点XXThreadSensitiveObject ,但是XXThreadSensitiveObject可以完成它的工作。 XXThreadAffinityProxy是一个简单的代理,除了确保调用正确的发生在右边的线程上, XXThreadMain只是从属线程的主程序和其他一些小的机制的持有者。 它本质上只是一个保留周期的解决方法,否则会在线程和拥有该线程的哲学所有权的代理之间创build。

在这里要知道的是,线程是一个相对较大的抽象,是一个有限的资源。 这种devise假设你将要做出一两件这样的事情,而且他们会长寿。 这种使用模式在包装期望线程关联的第三方库的上下文中是有意义的,因为无论如何这通常是一个单例,但是这种方法不会扩展到一小撮线程。

给你第一个问:

我想线程,发送消息的意思。 但我无法解释这是怎么发生的。

第二:我不会混用NSThread和GCD。 我认为会有比解决scheme更多的问题。 这是因为你最后一个Q:

每个块都在一个线程上运行。 至less这是完成的,因为一个块的线程迁移将是昂贵的。 但是队列中的不同块可以分配给许multithreading。 这对于并行队列是显而易见的,但对于串行也是如此。 (在实践中已经看到了这一点)

我build议把你的整个代码移到GCD。 一旦你方便,它使用起来非常简单,而且容易出错。

队列和线程之间根本没有映射,唯一例外的是主线程上始终运行的主队列。 任何以主队列为目标的队列当然也会在主线程上运行。 任何后台队列都可以在任何线程上运行,并且可以将线程从一个块执行更改为下一个。 串行队列和并发队列也是如此。

GCD维护一个线程池,根据块所属的队列确定的策略,它被用于执行块。 你不应该知道这些特定线程的任何事情。