GCD、NSThread 和 performSelector:onThread: 问题

Posted

技术标签:

【中文标题】GCD、NSThread 和 performSelector:onThread: 问题【英文标题】:GCD, NSThread, and performSelector:onThread: issues 【发布时间】:2015-04-28 07:30:36 【问题描述】:

我正在尝试调试一些包含以下错误消息的 ios 崩溃日志:

*** 由于未捕获的异常 '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];
    

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

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

我假设这是第二种选择,好像主线程真的已经终止了后台线程的崩溃无论如何都没有实际意义。

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

特别注意事项

这个方法注册到它的runloop 当前上下文,并且取决于该 runloop 在常规上运行 正确执行的基础。您可能会调用的一种常见上下文 这个方法并最终注册了一个不是 定期自动运行是在被 调度队列。如果您在运行时需要这种类型的功能 一个调度队列,你应该使用 dispatch_after 和相关的方法来 得到你想要的行为。

...我已修改我的代码,使其更喜欢使用 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 队列的直接映射?

【问题讨论】:

您可能想要解释为什么(如果有原因的话)您完全关心线程关联性(对于除主线程以外的任何线程。)如果您关心的唯一线程关联性是主线程线程,那么在绝大多数情况下,+[NSThread isMainThread]dispatch_async(dispatch_get_main_queue(), ...) 应该就是你所需要的。 @ipmcc - 代码本质上是为非线程安全的第三方库提供了一个线程安全的包装器,它要求对它的所有调用都在用于初始化它。虽然这是常见的做法,但至少在我正在使用的代码中,要在主线程上初始化库,这不是严格的要求,因此我对可以与任何线程一起使用的解决方案感兴趣。涵盖那些“假设”场景。 【参考方案1】:

鉴于您声明的目标是封装需要线程关联的第 3 方 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 只是从属线程的主例程和其他一些次要机制的持有者。它本质上只是一个保留循环的解决方法,否则会在线程和拥有线程哲学所有权的代理之间创建。

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

【讨论】:

谢谢,这是一个有趣的方法。我没有想过尝试使用NSProxy 来解决问题。【参考方案2】:

致你的第一个问题:

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

第二:我不会混合NSThread 和 GCD。我认为问题多于解决方案。这是因为你的最后一个问题:

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

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

【讨论】:

绝对转移到 GCD。首先,块比调用更简单、更易于维护。【参考方案3】:

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

GCD 维护一个线程池,用于根据块所属队列确定的策略执行块。您不应该对这些特定线程一无所知。

【讨论】:

以上是关于GCD、NSThread 和 performSelector:onThread: 问题的主要内容,如果未能解决你的问题,请参考以下文章

GCD、NSThread 和 performSelector:onThread: 问题

用于 CoreMotion 和准确计时目的的 NSThread、NSOperation 或 GCD?

九使用多线程——NSThread,GCD和NSOperation

带有异步回调的 NSThread 与 GCD

多线程之pthread, NSThread, NSOperation, GCD

iOS多线程NSThread,NSOperation和GCD详解