GCD, NSThread, and performSelector:onThread: issues



我正在尝试调试一些iOS崩溃日志,其中包含以下错误消息:

***由于未捕获的异常"NSDestinationInvalidException"而终止应用程序,原因:"***-[SomeClassperformSelector: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:...参考文件中的以下讨论:

特殊注意事项

此方法在其当前上下文,并且取决于在正则表达式上运行的运行循环正确执行的基础。您可以调用的一个常见上下文该方法,并最终注册到一个不是当被调度队列。如果在上运行时需要此类型的功能对于调度队列,应该使用dispatchafter和相关方法得到你想要的行为。

。。。我修改了我的代码,更喜欢使用GCD而不是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:总是在同一个线程上调用。

我们还可以说,如果我们在主线程上创建这个对象,那么我们的期望是主线程是受祝福的线程,所有调用都应该在主线程中,但如果它是从任何非主线程创建的,那么它应该生成自己的线程,这样它就可以保证线程的亲和力。(因为GCD管理的线程是短暂的,所以没有办法与GCD管理线程具有线程相关性。)

一种可能的实现方式如下:

// 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可以完成它的工作。XXThreadAffinityProxy是一个瘦代理,除了确保调用发生在正确的线程上之外,它什么也不做,而XXThreadMain只是从属线程的主例程和其他一些次要机制的持有者。它本质上只是保留循环的一种变通方法,否则保留循环将在线程和代理之间创建,代理对线程具有哲学所有权。

这里需要知道的是,线程是一个相对较重的抽象,并且是一个有限的资源。这个设计假设你要做一两个这样的东西,而且它们会长寿。这种使用模式在包装期望线程亲和性的第三方库的上下文中是有意义的,因为这通常是一个单例,但这种方法不会扩展到超过少数线程。

致您的第一个问题:

我认为线程,发送信息是有意义的。但我无法解释这是怎么发生的。

第二:我不会把NSThread和GCD混在一起。我认为问题会多于解决办法。这是因为你的最后一个问题:

每个块在一个线程上运行。至少这样做了,因为块的线程迁移成本很高。但是队列中的不同块可以分布到许多线程。这对于并行队列是显而易见的,但对于串行队列也是如此。(并且已经在实践中看到了这一点。)

我建议将您的整个代码转移到GCD。一旦你使用它很方便,它就很容易使用,而且不容易出错。

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

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

最新更新