Search code examples
iosobjective-cmultithreadingcocoansthread

how to run methods in one specific thread (not main thread)


I call a heartBeats method per 10ms in a specific thread(not main thread), how to call another method at any time in this same thread?

I subclass NSThread like this

@implementation MyThread
{
    NSTimeInterval _lastTimeInterval;
}

- (void)main
{

    while (true) {

        NSTimeInterval timeInterval = [[NSDate date] timeIntervalSince1970]*1000;

        if (timeInterval - _lastTimeInterval > 10)
        {
            [self heartBeats];

            _lastTimeInterval = timeInterval;
        }

    }

}

- (void)heartBeats
{
    NSLog(@"heart beats thread: %@", [NSThread currentThread].description);
}

@end

and run it like this

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    NSLog(@"main thread: %@", [NSThread currentThread].description);

    MyThread *myThread = [[MyThread alloc]init];
    [myThread start];
}


- (void)someMethod
{
    // do somthing
}

- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

@end

Now,here is the question, how to run - (void)someMethod in myThread?


Solution

  • The main method of your NSThread subclass is everything that runs on that thread. You cannot interrupt it to run other code without the main method's cooperation.

    What you really should do is throw out that entire loop and replace it with NSRunLoop and NSTimer.

    • NSRunLoop keeps the thread alive as long as there's something it will need to do, but also sleeps the thread until it needs to do something.
    • NSTimer does something on a repeating interval as long as it's scheduled on a run loop.

    You need your thread to do two things:

    • send the MyThread object a heartBeats message (you're doing this)
    • send the view controller a someMethod message (this is what you asked about)

    For the latter, you need one additional thing: A run loop source.

    So, clear out your main method and replace it with the following:

    1. Get the current NSRunLoop and store it in a local variable.
    2. Create an NSTimer with a 10-second interval, whose target is self and selector is heartBeats. (Slightly cleaner version: Have another method that takes an NSTimer *but ignores it, so your timer calls that method and that method calls heartBeats. The proper way to set up a timer is to give it a method that expects to be called with a timer, but, in practice, giving it a method that takes no arguments works, too.)
    3. If you didn't create the timer using scheduledTimerWith…, add it to the run loop. (The scheduledTimerWith… methods do this for you.)
    4. Create a run loop source and add it to the run loop.
    5. Call [myRunLoop run].

    Step 4 bears explaining:

    Core Foundation (but not Foundation; I don't know why) has something called “run loop sources”, which are custom objects that can be added to a run loop and will call something once signaled.

    Sounds like what you want, to call your view controller method!

    First, in the view controller, change myThread from a local variable in viewDidLoad to an instance variable. Rename it _myThread to make that clear.

    Next, add a delegate property to MyThread. This should be weak and have type id <MyThreadDelegate>. You'll also need to define a MyThreadDelegate protocol, which should have one method taking no arguments and returning nothing (void).

    You should now be able to set _myThread.delegate = self from the view controller, and implement in the view controller the method that you declared in the protocol. The view controller is now the delegate of its MyThread.

    In -[MyThread main], create a version-0 CFRunLoopSource. The Create function takes a “context” structure, containing, among other things, the version (0), an “info” pointer (set this to self, i.e., the MyThread) and a Perform callback (a function, which will be called with the info pointer as its only argument).

    In your perform callback, you'll need to do something like this:

    MyThread *self = (__bridge MyThread *)info;
    [self fireDelegateMessage];
    

    In MyThread, implement that fireDelegateMessage method. In there, send self.delegate the message you declared in your protocol.

    Next, add a public method to MyThread (i.e., declare it in MyThread.h as well as implementing it in MyThread.m) named something like “requestDelegateMessage”. In this method, call CFRunLoopSourceSignal on the run loop source. (The documentation for that function suggests that you also need to call CFRunLoopWakeUp on the thread's CFRunLoop. Try it without first.)

    Lastly, when the view controller wants someMethod to be called on that thread, call [_myThread requestDelegateMessage].

    So:

    1. the view controller calls requestDelegateMessage
    2. requestDelegateMessage signals the run loop source (and wakes up the run loop, if that is needed)
    3. the run loop source calls the perform callback on the MyThread's thread
    4. the perform callback calls fireDelegateMessage on the MyThread's thread
    5. fireDelegateMessage calls the view controller's implementation of the delegate method on the MyThread's thread
    6. the view controller calls someMethod on the MyThread's thread