GCD、NSThread 和 performSelector:onThread:问题

GCD, NSThread, and performSelector:onThread: issues

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

*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[SomeClass performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform

代码的相关部分是:

- (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: 被传递给应用程序主线程的引用,所以除非整个应用程序正在终止,否则它不可能终止。

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

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

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

Special Considerations

This method registers with the runloop of its current context, and depends on that runloop being run on a regular basis to perform correctly. One common context where you might call this method and end up registering with a runloop that is not automatically run on a regular basis is when being invoked by a dispatch queue. If you need this type of functionality when running on a dispatch queue, you should use dispatch_after and related methods to get the behavior you want.

...我修改了我的代码以更喜欢使用 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和GCD。我认为问题多于解决方案。这是因为你的最后一个问题:

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

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

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

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

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

这里要知道线程是一个相对重的抽象,并且是一种有限的资源。该设计假设您将制作其中的一两个,并且它们将长期存在。这种使用模式在包装需要线程关联的第 3 方库的上下文中很有意义,因为无论如何这通常都是单例,但这种方法不会扩展到超过一小部分线程。