objective-cgrand-central-dispatchnsrunlooprunloopcfrunloop

Need some clarifications about dispatch_queue_create and RunLoop. Sharing RunLoop between them


I'm developing iOS framework with Objective-C.

I create a dispatch_queue_t by using dispatch_queue_create. And call CFRunLoopRun() for run the runloop in the queue.

But, It looks like the dispatch_queue_t has share the RunLoop. Some classes has add an invalid timer, and when I call the CFRunLoopRun(), It crashed on my side.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.queue1 = dispatch_queue_create("com.queue1", DISPATCH_QUEUE_CONCURRENT);
    self.queue2 = dispatch_queue_create("org.queue2", DISPATCH_QUEUE_CONCURRENT);
}

- (IBAction)btnButtonAction:(id)sender {
    
    dispatch_async(self.queue1, ^{
        
        NSString *runloop = [NSString stringWithFormat:@"%@", CFRunLoopGetCurrent()];
        runloop = [runloop substringWithRange:NSMakeRange(0, 22)];
        NSLog(@"Queue1 %p run: %@", self.queue1, runloop);
        
        //NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(wrongSeletor:) userInfo:nil repeats:NO];
        //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    });
    
    dispatch_async(self.queue2, ^{
        NSString *runloop = [NSString stringWithFormat:@"%@", CFRunLoopGetCurrent()];
        runloop = [runloop substringWithRange:NSMakeRange(0, 22)];
        NSLog(@"Queue2 %p run: %@", self.queue2, runloop);
        
        CFRunLoopRun();
    });
}

Some time they take same RunLoop:

https://i.sstatic.net/wGcv3.png

=====

You can see the crash by uncomment the code of NSTimer. The NSTimer has been added in queue1, but it still running when call CFRunLoopRun() in queue2.

I have read some description like: need some clarifications about dispatch queue, thread and NSRunLoop

They told that: system creates a run loop for the thread. But, in my check, they are sharing the RunLoop.

This is sad for me, because I facing that crashes happen when calling CFRunLoopRun() on production.


Solution

  • tl;dr

    There are several problems with this run loop code. But, if the intent was simply to run a timer on a dispatch queue, we would generally use a dispatch source timer, eliminating much of this confusion.


    The typical run loop pattern entails creating a dedicated thread, getting a run loop for that thread, and calling one of the “run” methods to spin on that thread, processing run loop events, such as timer events. (See point 2 in this answer for an example.)

    A dispatch queue is a different approach. When we dispatch a “work item” to a dispatch queue, that queue will fetch a (random) worker thread from a pool of threads, run that work item on that worker thread, and, when the work item is done, return that worker thread to the thread pool, making it available for future dispatches.

    So that leads us to several observations regarding your code snippet:

    1. The dispatch to queue1 makes no sense. You print a run loop, but do not “run” on it, but instead just finish, thereby returning the worker thread to the thread pool. That thread is now available to be used again by future GCD dispatches. It would have only made sense if you spun on that run loop with a “run” method, thereby tying that thread to that run loop and preventing GCD from reusing that thread.

    2. Using a concurrent queue in this context is confusing. Run loops spin on a given thread. Creating a concurrent queue for this purpose makes no sense. What work will be done concurrently on that queue while the run loop spins on one of the worker threads? It suggests a conceptual misunderstanding of how run loops work.

    3. The idea of indefinitely spinning on a run loop on a GCD worker thread, at all, is ill-advised. The general concept of dispatch queues is that they achieve efficiency by fetching an existing worker thread from the pool, performing the work item, and returning that thread to the pool. The idea of indefinitely tying up a worker thread is arguably antithetical to the motivating idea behind GCD. You can do it, but generally, we would avoid tying up a GCD worker thread indefinitely.

    4. If you are academically interested in how run loops work, this exploration is fine. But if you intended to just run a timer on a dispatch queue, then run loops and NSTimer is an antiquated, pre-GCD approach. A dispatch timer would be much simpler (see below).


    If you want to run something on a dispatch queue, we generally do not use NSTimer. Instead, we would use a dispatch timer. That requires no run loop at all.

    Unlike a NSTimer we need to keep our own strong reference to the timer:

    @property (nonatomic, strong) dispatch_source_t timer;
    

    Then, to start a timer:

    - (void)startTimer {
        dispatch_queue_t queue = dispatch_queue_create("com.domain.app.timer", DISPATCH_QUEUE_SERIAL);
        self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_source_set_timer(self.timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
        dispatch_source_set_event_handler(self.timer, ^{
            // this is called when the timer fires
        });
        dispatch_resume(self.timer);
    }
    

    To stop the timer, release it:

    - (void)stopTimer {
        self.timer = nil;
    }