GCD,NSThread和performSelector:onThread:问题

时间:2015-04-28 07:30:36

标签: ios objective-c multithreading grand-central-dispatch

我试图调试一些包含以下错误消息的iOS崩溃日志:

  

***由于未捕获的异常终止应用' NSDestinationInvalidException',原因:' *** - [SomeClass   performSelector:onThread:withObject:waitUntilDone:modes:]:target   线程在等待执行时退出

代码的相关部分是:

- (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];
    }
}

这与问题discussed here类似,只是我没有尝试取消onThread:主题。实际上,在我的情况下,onThread:正在传递对应用程序主线程的引用,因此除非整个应用程序终止,否则它不应该终止。

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

我认为它是第二个选项,好像主线程真的已经终止了后台线程的崩溃,无论如何都是没有意义。

考虑到这一点,并基于performSelector:onThread:...performSelector:onThread:...的以下讨论:

  

特别注意事项

     

此方法向其runloop注册   当前上下文,并取决于在常规上运行的runloop   正确执行的基础。您可以打电话的一个常见背景   这个方法最终注册了一个不是的runloop   自动运行是在被a调用时   调度队列。如果在运行时需要此类功能   一个调度队列,你应该使用dispatch_after和相关的方法来   得到你想要的行为。

...我修改了我的代码,希望在- (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,如下所示:

NSThread

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

无论如何,主要的问题是当目标线程是主线程时,只有一种显而易见的方法来使用GCD。就我而言,这是事实,但无论目标线程是否是主线程,我都希望能够使用GCD。

所以更重要的问题是,有没有办法从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

这是可能的,还是没有可以应用的if type(var) is int: 到GCD队列的直接映射?

3 个答案:

答案 0 :(得分:2)

到你的第一个问:

我认为发送消息的线程意味着。但我无法解释这是怎么发生的。

第二:我不会混淆Q(answer__iregex=r'[[:space:]]') 和GCD。我认为问题会多于解决方案。这是因为你的上一个Q:

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

我建议将整个代码移动到GCD。一旦您方便使用它,它非常易于使用且不易出错。

答案 1 :(得分:2)

鉴于您声明的包装需要线程关联的第三方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只是下级线程主程序和其他一些小机制的持有者。它本质上只是一个保留周期的解决方法,否则将在线程和具有该线程的哲学所有权的代理之间创建。

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

答案 2 :(得分:1)

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

GCD维护一个线程池,该线程池用于根据块所属队列确定的策略执行块。你不应该对这些特定线程有任何了解。