4

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?

Grey
  • 253
  • 4
  • 15
  • busy polling like that is awful code anywhere, but particularly on a mobile device – Paulw11 Sep 24 '15 at 03:22
  • @Paulw11 yeah, but that is out of the question, actually, `heartBeats` is an API I have to call like this to keep this long connection. : ( – Grey Sep 24 '15 at 03:31
  • Replacing your `if` code with `[NSThread sleepForTimeInterval:0.007];` reduced CPU consumption from 98% to 2% and resulted in intervals between 7.6 ms and 9.8ms. Using `[NSThread sleepForTimeInterval:0.0002];` with your if code gave intervals closer to 10.3ms and used 9% CPU but the short answer is you will have to use some sort of synchronisation and a shared memory data structure to have your loop determine that it should execute some other method. You can't simply dispatch a method on a thread the way you can with a GCD queue – Paulw11 Sep 24 '15 at 03:52
  • @Paulw11 however, GCD queue cannot ensure the `- (void)someMethod` and `- (void)heartBeats` are both called in a same thread... – Grey Sep 24 '15 at 04:11
  • then you will need to set some semaphore that your thread recognises and uses to execute some other method – Paulw11 Sep 24 '15 at 04:44
  • @Paulw11 Would you please give me more information? A demo will be perfect. thank you. – Grey Sep 24 '15 at 05:40
  • @Paulw11 As for my `if` code, you are right, it's awful, thanks for your advice, it helps a lot. – Grey Sep 24 '15 at 09:33
  • You can refer to the QT source code, which contains the timer event loop. – gao.xiangyang May 27 '23 at 10:58

1 Answers1

4

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
Peter Hosey
  • 95,783
  • 15
  • 211
  • 370
  • Thanks for your excellent answer, it works! But I notice this, if I give a large enough time interval to the timer, `someMethod` delays. – Grey Oct 09 '15 at 06:00
  • @GaojinHsu: That's what timers do, and why it's almost always better to do something as-needed via a delegate method or dispatched block rather than periodically via a timer. – Peter Hosey Oct 10 '15 at 04:57
  • @GaojinHsu: Also, I misread the part where you said 10 ms and not 10 seconds. 10 ms is *really, really frequent!* It's literally faster than the screen updates (60 Hz = 1/60 of a second = 16+2/3 ms), and almost certainly faster than whatever your timer actually does—meaning that *doing the work* will keep the timer from updating as fast as you wanted it to. A timer probably isn't the best way to do whatever you're doing in the first place, and if it is, it shouldn't be that frequent. – Peter Hosey Oct 10 '15 at 04:59