Search code examples
iosgrand-central-dispatchconcurrent-queue

Why weird behavior of concurrent queue?


I am trying to understanding the concurrent queue of iOS GCD. The I made some code to test it, but found something weird. code as below:

_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);

for (int index = 0; index < 3; ++index) {
    dispatch_sync(_syncQueue, ^{

        NSLog(@"sync@@@@@@ >>>>    %d ",index);
        sleep(1);
        NSLog(@"sync@@@@@@ <<<<    %d ",index);
    });
}

for (int index = 3; index < 6; ++index) {
    dispatch_async(_syncQueue, ^{

        NSLog(@"sync===== >>>>    %d ",index);
        sleep(1);
        NSLog(@"sync===== <<<<    %d ",index);
    });
}

for (int index = 6; index < 9; ++index) {
    dispatch_sync(_syncQueue, ^{

        NSLog(@"sync***** >>>>    %d ",index);
        sleep(1);
        NSLog(@"sync***** <<<<    %d ",index);
    });
}

The execution results are below:

sync@@@@@@ >>>>    0
sync@@@@@@ <<<<    0
sync@@@@@@ >>>>    1
sync@@@@@@ <<<<    1
sync@@@@@@ >>>>    2
sync@@@@@@ <<<<    2
sync***** >>>>    6
sync===== >>>>    4
sync===== >>>>    3
sync===== >>>>    5
sync***** <<<<    6
sync***** >>>>    7
sync===== <<<<    4
sync===== <<<<    5
sync===== <<<<    3
sync***** <<<<    7
sync***** >>>>    8
sync***** <<<<    8

I'm confused, and cannot understand why it's run like this.

  1. why until 1st loop runs completely finished then 2nd and 3rd loop can start to run, to me, 1st loop should can be interrupted by 2nd loop at least, since 2nd loop can create new threads to execute.

  2. Why the 3rd loop start earlier than the 2nd loop?

  3. Why 3rd loop cannot run as the 1st loop which runs without interrupted by other tasks?


Solution

  • You asked:

    1. why until 1st loop runs completely finished then 2nd and 3rd loop can start to run, to me, 1st loop should can be interrupted by 2nd loop at least, since 2nd loop can create new threads to execute.

    That's because you've used dispatch_sync. That effectively says "stop the current thread from proceeding until the dispatched task finishes." So, that first loop won't even get to the next iteration of its own loop until the task dispatched with prior dispatch_sync finishes.

    If you look at the diagram below, the dispatch_sync calls are the red Ⓢ flags. You can see that it doesn't even get to dispatching the second iteration of this first loop until the first dispatched task finishes.

    1. Why the 3rd loop start earlier than the 2nd loop ?

    It's a classic race condition. You're dispatching a lot of tasks to a concurrent queue (all of the global queues are concurrent queues), which technically starts them in the order that they were queued, but since they're allowed to run concurrently, they're running at the same time and you have no assurances as to which will actually reach its respective NSLog statements first. If you look at the timestamps associated with those NSLog statements are incredibly close to each other (unlike the NSLog statements of the first loop).

    Note, while you technically have no assurances as to whether tasks dispatched by the second or third loop will start first, there are two interesting details:

    1. We can be relatively confident that the subsequent iterations of the third loop (namely iterations 7 and 8) will not start before the second loop's dispatched tasks, because again, you dispatch everything in the third loop synchronously. So, for example, it won't even try to dispatch iteration 7 until the execution of iteration 6 is done (whereas the second loop had already dispatched its tasks asynchronously and those tasks will run unabated on that concurrent queue).

    2. Note, while you have no assurances about the timing of tasks dispatched by second loop and that first task dispatched by third loop, in practice, you'll generally see that first task of the third loop start more quickly because of optimizations built in to dispatch_sync. The dispatch_async used by the second loop has to do a lot of work, namely GCD must get a worker thread from the pool and and start the task on that thread. But the dispatch_sync of the third loop, as an optimization, often just runs the dispatched task on the current thread. (If the thread has to wait for the dispatched task anyway, why not just use it to run the task and avoid the context switch altogether.)

      This is a technical detail that I'd suggest you don't worry about, but does explain why you'll often see dispatch_sync tasks start more quickly than dispatch_async started on the same concurrent queue at roughly the same time.

    So, in the diagram below, the dispatch calls (the red Ⓢ) for iterations 3-5 (second loop) and iteration 6 (the first iteration of third loop) happen so close together that the flags are superimposed on top of each other. But you can see the timing of those in the list below the chart.

    1. Why 3rd loop cannot run as the 1st loop which runs without interrupted by other tasks ?

    The issue isn't that first loop ran "without interruption" but merely that there wasn't anything else running on the queue and because it was running synchronously, nothing else was going to start on it until loop 1 finished. Whereas the third loop dispatched iteration #6 at nearly the same time as all of the second loop's iterations #3 through 5.

    I think it's illustrative to look at a timeline (produced by Instruments' "Points of Interest" tool) of these nine dispatched tasks:

    enter image description here

    The first three light blue tasks represent the first loop. The purple tasks are second loop. The orange tasks are the third loop. The dispatch_sync and dispatch_async calls are indicated with the red Ⓢ flags.

    As you can see, the first and third loop show the same behavior, namely that because you're dispatching these blocks synchronously, it can't even try to dispatch the next task until the prior synchronously dispatched task finishes running. But the second loop ran incredibly quickly, dispatching all three tasks one right after the other, very quickly, and those tasks ran concurrently with respect to each other while the main thread carried on proceeded dispatching the third loop while the tasks dispatched by the second loop were still running.