Reputation: 54806
I'm attempting to debug some iOS crash logs that contain the following error message:
*** Terminating app due to uncaught exception 'NSDestinationInvalidException', reason: '*** -[SomeClass performSelector:onThread:withObject:waitUntilDone:modes:]: target thread exited while waiting for the perform
The relevant section of the code is:
- (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];
}
}
This is similar to the issue discussed here, except that I'm not trying to cancel my onThread:
thread. In fact, in my case onThread:
is being passed a reference to the application's main thread, so it should not be possible for it to terminate unless the entire app is terminating.
So the first question is, is the "target" thread referred to in the error message the one I'm passing to onThread:
, or the one that's waiting for the invocation to complete on the onThread:
thread?
I've assumed that it's the second option, as if the main thread really has terminated the crash of the background thread is kind of moot anyways.
With that in mind, and based upon the following discussion from the reference docs for performSelector: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.
...I've modified my code to prefer the use of GCD over performSelector:onThread:...
, as follows:
- (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];
}
}
Which seems to work fine (though no idea if it fixes the crash, as it's an exceedingly rare crash). Perhaps someone can comment on whether this approach is more or less prone to crashing than the original?
Anyways, the main problem is that there's only an obvious way to use GCD when the target thread is the main thread. In my case, this is true, but I'd like to be able to use GCD regardless of whether or not the target thread is the main thread.
So the more important question is, is there a way to map from an arbitrary NSThread
to a corresponding queue in GCD? Ideally something along the lines of dispatch_queue_t dispatch_get_queue_for_thread(NSThread* thread)
, so that I can revise my code to be:
- (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];
}
}
Is this possible, or is there not a direct mapping from NSThread
to GCD queue that can be applied?
Upvotes: 2
Views: 1349
Reputation: 29886
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.
Upvotes: 2
Reputation: 32622
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.
Upvotes: 1
Reputation: 16650
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.
Upvotes: 2