Correct usage of secondary NSThread with NSRunLoop

569 views Asked by At

I have a performance-sensitive code, working with frames of video-playback in real time. I have some work here which can be parallelised and since it's a performance-sensitive code where latency is the key I decided to go with NSThread instead of GCD.

What I need: I need to have NSThread which will be scheduled with some work at a certain period of time. After the thread is done with its work, it goes to sleep till new work arrives.

Unfortunately, there're not so much info about correct techniques of NSThread usage in the internet, so I assembled my routine based on information bits I managed to find.

You can find the whole workflow above:

1) Init my NSThread. This code is launched only once as it's supposed to.

_myThread = [[NSThread alloc] initWithTarget:self selector:@selector(_backgroundMethod) object:nil];
 _myThread.threadPriority = 0.8;    //max priority is 1.0. Let's try at 0.8 and see how it performs
[_myThread start];

2) _backgroundMethod code:

- (void)_backgroundMethod
{
    NSLog(@"Starting the thread...");
    [NSTimer scheduledTimerWithTimeInterval:FLT_MAX target:self selector:@selector(doNothing:) userInfo:nil repeats:YES];

    BOOL done = false;
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    do {
        [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    } while (!done);
}

- (void)doNothing:(NSTimer *)sender { }

3) When thread has something to work with I make the next call:

[self performSelector:@selector(_doSomeCalculation) onThread:_myThread withObject:nil waitUntilDone:NO];

Which calls next method:

- (void) _doSomeCalculation
{
    //do some work here
}

So my question would be:

1) When I init NSThread, I pass a selector. What's the purpose of that selector? As far as I understand, the only purpose of this selector is controlling thread's RunLoop and I should not do any calculations here. So I'm engaging NSRunLoop with an infinite timer just to keep it alive without constant running a while loop. Is that right approach?

2) If I can do a calculation in the selector I'm passing at NSThread init phase - how can I signal NSRunLoop to do one loop those without using performSelector? I think I should not pass the exact same method with performSelector because it would be a mess, right?

I've read a lot of info Apple provided but all of this is almost theoretical and those code samples which provided confused me even more..

Any clarification would be very appreciated. Thanks in advance!

EDIT:

Also a question - how I can calculate a desired stackSize for my thread? Is there any technique to do that?

1

There are 1 answers

8
Rob Napier On

since it's a performance-sensitive code where latency is the key I decided to go with NSThread instead of GCD.

You should generally not do that unless you have a solid understanding of GCD and know exactly what you're giving up. Used correctly, GCD is highly optimized, and integrated very closely with the OS. It's particularly surprising to be using NSThread by hand, but then doing your work in ObjC with performSelector and run loops. Calling performSelector this way is introducing the same kind of unknown latency as a GCD serial queue. If the thread is already busy, then you'll queue the selector, exactly like queuing a block (but you'll add the overhead of objc_msgSend). A GCD concurrent queue would perform better. In order to match GCD, you would need to implement a proper thread pool (or at least add cancelation). Done well, this can be better than GCD for specific use cases, but it has to be done very well, and that's complicated.

As the Threading Programming Guide notes:

Note: Although good for occasional communication between threads, you should not use the performSelector:onThread:withObject:waitUntilDone: method for time critical or frequent communication between threads.

If you want low-latency communication between threads, you'll typically want to use semaphores such as NSConditionLock rather than a runloop.

That said, let's get to the actual questions.

The performSelector:onThread:... interface is generally used for one-shot operations. The more common way to implement a long-running, dedicated thread is to subclass NSThread and override main. Something like this (this is thrown together and untested, based on code from the Threading Programming Guide; I've done all my high-performance work in GCD for years now, so I probably have goofed something here).

#import "WorkerThread.h"

#define NO_DATA 1
#define HAS_DATA 2

@implementation WorkerThread
static NSConditionLock *_condLock;
static NSMutableArray *_queue;

+ (void)initialize
{
    if (self == [WorkerThread class]) {
        _condLock = [[NSConditionLock alloc] initWithCondition:NO_DATA];
        _queue = [NSMutableArray new];
    }
}

- (void)main
{
    // Until cancelled, wait for data, and then process it.
    while (!self.cancelled)
    {
        [_condLock lockWhenCondition:HAS_DATA];
        id data = [_queue firstObject];
        [_queue removeObjectAtIndex:0];
        [_condLock unlockWithCondition:(_queue.count == 0 ? NO_DATA : HAS_DATA)];

        // Process data
    }
}

// Submit work to the queue
+ (void)submitWork:(id)data {
    [_condLock lock];
    [_queue addObject:data];
    [_condLock unlockWithCondition:HAS_DATA];
}

@end

You'd spawn up some threads like:

workers = @[[WorkerThread new], [WorkerThread new]];
for (worker in workers) {
    [worker start];
}

[WorkerThread submitWork: data];

And you'd shut down threads like:

for (worker in workers) {
    [worker cancel];
}