GCD, NSThread, and performSelector:onThread: issues

后端 未结 3 1175
日久生厌
日久生厌 2021-01-23 00:58

I\'m attempting to debug some iOS crash logs that contain the following error message:

*** Terminating app due to uncaught exception \'NSDestinationInvali

相关标签:
3条回答
  • 2021-01-23 01:07

    There is no mapping at all between queues and threads, with the sole exception of the main queue which always runs on the main thread. Any queue which targets the main queue will, of course, also run on the main thread. Any background queue can run on any thread, and can change thread from one block execution to the next. This is equally true for serial queues and for concurrent queues.

    GCD maintains a thread pool which gets used for executing blocks according to the policies determined by the queue to which the block belongs. You are not supposed to know anything about those particular threads.

    0 讨论(0)
  • 2021-01-23 01:15

    Given your stated goal of wrapping a 3rd party API that requires thread affinity, you might try something like using a forwarding proxy to ensure methods are only called on the correct thread. There are a few tricks to doing this, but I managed to whip something up that might help.

    Let's assume you have an object XXThreadSensitiveObject with an interface that looks something like this:

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

    And the goal is for -foo, -bar and -addX:Y: to always be called on the same thread.

    Let's also say that if we create this object on the main thread, then our expectation is that the main thread is the blessed thread and all calls should be on the main thread, but that if it's created from any non-main thread, then it should spawn its own thread so it can guarantee thread affinity going forward. (Because GCD managed threads are ephemeral, there is no way to have thread affinity with a GCD managed thread.)

    One possible implementation might look like this:

    // 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
    

    The ordering here is a little wonky, but XXThreadSensitiveObject can just do its work. XXThreadAffinityProxy is a thin proxy that does nothing other than ensuring that the invocations are happening on the right thread, and XXThreadMain is just a holder for the subordinate thread's main routine and some other minor mechanics. It's essentially just a workaround for a retain cycle that would otherwise be created between the thread and the proxy which has philosophical ownership of the thread.

    The thing to know here is that threads are a relatively heavy abstraction, and are a limited resource. This design assumes that you're going to make one or two of these things and that they will be long lived. This usage pattern makes sense in the context of wrapping a 3rd party library that expects thread affinity, since that would typically be a singleton anyway, but this approach won't scale to more than a small handful of threads.

    0 讨论(0)
  • 2021-01-23 01:17

    To your first Q:

    I think the thread, sending the message is meant. But I cannot explain how this can happen.

    Second: I would not mix NSThread and GCD. I think that there will be more problems than solutions. This is because of your last Q:

    Each block is running on one thread. At least this is done, because thread migration for a block would be expensive. But different blocks in a queue can be distributed to many threads. This is obvious for parallel queues, but true for serial, too. (And have seen this in practice.)

    I recommend to move your whole code to GCD. Once you are convenient with it, it is very easy to use and less error prone.

    0 讨论(0)
提交回复
热议问题