1

I have a question about dispatch_barrier and the target queue. I have a custom serial queue and custom concurrent queue and I set the target queue of the serial queue to a concurrent queue which then targets a global concurrent queue:

(serial queue) -> (concurrent queue) -> (global concurrent queue)

What happens when I dispatch_barrier blocks on the serial queue? Will It block the execution of the blocks submitted to the concurrent queue too, or just the execution blocks in the serial queue only? Or if I dispatch_barrier blocks to the non-global concurrent queue, will It block the execution of blocks submitted to the serial queue too, or just the execution of blocks in the non-global concurrent queue only?

Thank you for your interest. :)

1 Answers1

6

Submitting a dispatch_barrier_async to a serial queue is no different than dispatch_async because the queue is serial, so there couldn't be any readers to keep out, since only one block can execute on a serial queue at a time. Put differently, every block is a "barrier block" on a serial queue.

If you dispatch_barrier_async to the non-global concurrent queue, then readers will be kept out of THAT queue, but not the global queue it targets. It acts as a barrier only to the queue it is submitted to.

If you want to further convince yourself, think of it this way: All queues ultimately target one of the global concurrent queues (background, low, default, and high priority). With that in mind, if dispatch_barrier* to any queue transitively caused a barrier on the global queue that the submitted-to queue ultimately targeted, then it would be trivial to use dispatch_barrier* to starve out all other clients of GCD (by submitting a barrier block to 4 private concurrent queues, each of which targeted a different priority global queue.) That would be totally bogus.

Coming at it from the other direction: dispatch_barrier* is useful specifically because you can create arbitrary units of mutual exclusion (i.e. non-global concurrent queues).

In short: the queue you submit to is the unit of "protection" (or "barrier-ness").

EDIT: If you're willing to take the above at face value, you can stop reading, but in an effort to give some more clarity here, I coded up a quick example to prove my claims. As some background, this is from Apple's documentation:

If the queue you pass to this function [disaptch_barrier_async] is a serial queue or one of the global concurrent queues, this function behaves like the dispatch_async function.

This means that a disaptch_barrier_async submitted to a serial queue will have no external effect, nor will a disaptch_barrier_async submitted to a global queue. Rather than merely appeal to authority, I'll prove these two claims.

Barrier block submitted to private serial queue

Here's the code:

static void FakeWork(NSString* name, NSTimeInterval duration, dispatch_group_t groupToExit);

- (void)applicationDidFinishLaunching:(NSNotification *)aNotification
{
    dispatch_queue_t privateSerialQueue = dispatch_queue_create("", DISPATCH_QUEUE_SERIAL);
    dispatch_queue_t privateConcurQueue = dispatch_queue_create("", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalConcurQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    dispatch_set_target_queue(privateSerialQueue, privateConcurQueue);
    dispatch_set_target_queue(privateConcurQueue, globalConcurQueue);

    // Barrier block submitted to serial queue. Per the docs, we expect this to have no effect
    // and behave like dispatch_async. So, we expect this to run to completion in 15s.
    {
        NSString* testDesc = @"Checking for effects of barrier block on serial queue";
        dispatch_suspend(globalConcurQueue);
        dispatch_group_t group = dispatch_group_create();
        NSDate* start = [NSDate date];
        NSLog(@"%@\nStarting test run at: %@", testDesc, start);

        // We expect these to take 15s total
        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A1: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_barrier_async(privateSerialQueue, ^{ FakeWork(@"A2: 5s BARRIER Job on privateSerialQueue", 5.0, group); });
        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A3: 5s Job on privateSerialQueue", 5.0, group); });

        // So we'll make 3 15s jobs each for the privateConcurrentQueue and globalConcurrentQueue
        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B1: 15s Job on privateConcurQueue", 15.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B2: 15s Job on privateConcurQueue", 15.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B3: 15s Job on privateConcurQueue", 15.0, group); });

        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(globalConcurQueue, ^{ FakeWork(@"C1: 15s Job on globalConcurQueue", 15.0, group); });
        dispatch_async(globalConcurQueue, ^{ FakeWork(@"C2: 15s Job on globalConcurQueue", 15.0, group); });
        dispatch_async(globalConcurQueue, ^{ FakeWork(@"C3: 15s Job on globalConcurQueue", 15.0, group); });

        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

        NSDate* end = [NSDate date];
        NSLog(@"Test run finished at: %@ duration: %@", end, @([end timeIntervalSinceDate: start]));
    }
}

static void FakeWork(NSString* name, NSTimeInterval duration, dispatch_group_t groupToExit)
{
    NSDate* start = [NSDate date];
    NSLog(@"Starting task: %@ withDuration: %@ at: %@", name, @(duration), start);
    while (1) @autoreleasepool
    {
        NSTimeInterval t = [[NSDate date] timeIntervalSinceDate: start];
        if (t >= duration)
        {
            break;
        }
        else if ((t + 0.0005) < duration)
        {
            usleep(50);
        }
    }
    NSDate* end = [NSDate date];
    duration = [end timeIntervalSinceDate: start];
    NSLog(@"Finished task: %@ withRealDuration: %@ at: %@", name, @(duration), end);
    if (groupToExit)
    {
        dispatch_group_leave(groupToExit);
    }
}

If a dispatch_barrier_async had any effect at all on the targeted queue, we would expect this to take more than 15 seconds, but here's the output:

Checking for effects of barrier block on serial queue
Starting test run at: 2013-09-19 12:16:25 +0000
Starting task: C1: 15s Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Starting task: C2: 15s Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Starting task: C3: 15s Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Starting task: A1: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:16:25 +0000
Starting task: B1: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Starting task: B2: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Starting task: B3: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:16:25 +0000
Finished task: A1: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:16:30 +0000
Starting task: A2: 5s BARRIER Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:16:30 +0000
Finished task: A2: 5s BARRIER Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:16:35 +0000
Starting task: A3: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:16:35 +0000
Finished task: C1: 15s Job on globalConcurQueue withRealDuration: 15.00000900030136 at: 2013-09-19 12:16:40 +0000
Finished task: C2: 15s Job on globalConcurQueue withRealDuration: 15 at: 2013-09-19 12:16:40 +0000
Finished task: C3: 15s Job on globalConcurQueue withRealDuration: 15 at: 2013-09-19 12:16:40 +0000
Finished task: B1: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:16:40 +0000
Finished task: B2: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:16:40 +0000
Finished task: A3: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:16:40 +0000
Finished task: B3: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:16:40 +0000
Test run finished at: 2013-09-19 12:16:40 +0000 duration: 15.00732499361038

Barrier block submitted to global concurrent queue

Let's also verify the point from the documentation that barrier blocks submitted to the global concurrent queue have no barrier effect. Here's some code (just the differences from the first example):

    {
        NSString* testDesc = @"Barrier block submitted to globalConcurQueue";

        dispatch_group_t group = dispatch_group_create();
        NSDate* start = [NSDate date];
        NSLog(@"%@\nStarting test run at: %@", testDesc, start);

        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A1: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A2: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A3: 5s Job on privateSerialQueue", 5.0, group); });

        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B1: 15s Job on privateConcurQueue", 15.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B2: 15s Job on privateConcurQueue", 15.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B3: 15s Job on privateConcurQueue", 15.0, group); });

        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_async(globalConcurQueue, ^{ FakeWork(@"C1: 15s Job on globalConcurQueue", 15.0, group); });
        dispatch_barrier_async(globalConcurQueue, ^{ FakeWork(@"C2: 15s BARRIER Job on globalConcurQueue", 15.0, group); });
        dispatch_async(globalConcurQueue, ^{ FakeWork(@"C3: 15s Job on globalConcurQueue", 15.0, group); });

        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
        NSDate* end = [NSDate date];
        NSLog(@"Test run finished at: %@ duration: %@", end, @([end timeIntervalSinceDate: start]));
    }

If the barrier block submitted to the global concurrent queue had any effect, we would expect this to take longer than 15s, but here's the output:

Barrier block submitted to globalConcurQueue
Starting test run at: 2013-09-19 12:33:28 +0000
Starting task: C1: 15s Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Starting task: C2: 15s BARRIER Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Starting task: C3: 15s Job on globalConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Starting task: B1: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Starting task: A1: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:33:28 +0000
Starting task: B2: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Starting task: B3: 15s Job on privateConcurQueue withDuration: 15 at: 2013-09-19 12:33:28 +0000
Finished task: A1: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:33:33 +0000
Starting task: A2: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:33:33 +0000
Finished task: A2: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:33:38 +0000
Starting task: A3: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:33:38 +0000
Finished task: C1: 15s Job on globalConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: C2: 15s BARRIER Job on globalConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: C3: 15s Job on globalConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: B2: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: B3: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: B1: 15s Job on privateConcurQueue withRealDuration: 15 at: 2013-09-19 12:33:43 +0000
Finished task: A3: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:33:43 +0000
Test run finished at: 2013-09-19 12:33:43 +0000 duration: 15.00729995965958

Barrier block submitted to private concurrent queue

The next thing to test is the effects of barrier blocks submitted to the private concurrent queue. Since the serial queue targets the private concurrent queue, I expect blocks submitted to the serial queue to be held up for barrier blocks submitted to the private concurrent queue. And indeed, that's the case. Here's the code:

    // Barrier block submitted to private concurrent queue.
    {
        NSString* testDesc = @"Checking for effects of barrier block on private concurrent queue";
        dispatch_suspend(globalConcurQueue);
        dispatch_group_t group = dispatch_group_create();
        NSDate* start = [NSDate date];
        NSLog(@"%@\nStarting test run at: %@", testDesc, start);

        // Make 3 5s jobs on the private concurrent queue and make the middle one a barrier, which should serialize them
        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);
        dispatch_group_enter(group); dispatch_group_enter(group); dispatch_group_enter(group);

        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A1: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B1: 5s Job on privateConcurQueue", 5.0, group); });

        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A2: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_barrier_async(privateConcurQueue, ^{ FakeWork(@"B2: 5s BARRIER Job on privateConcurQueue", 5.0, group); });

        dispatch_async(privateSerialQueue, ^{ FakeWork(@"A3: 5s Job on privateSerialQueue", 5.0, group); });
        dispatch_async(privateConcurQueue, ^{ FakeWork(@"B3: 5s Job on privateConcurQueue", 5.0, group); });

        dispatch_group_wait(group, DISPATCH_TIME_FOREVER);

        NSDate* end = [NSDate date];
        NSLog(@"Test run finished at: %@ duration: %@", end, @([end timeIntervalSinceDate: start]));
    }

And here's the output:

Checking for effects of barrier block on private concurrent queue
Starting test run at: 2013-09-19 12:24:17 +0000
Starting task: B1: 5s Job on privateConcurQueue withDuration: 5 at: 2013-09-19 12:24:17 +0000
Starting task: A1: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:24:17 +0000
Finished task: A1: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:24:22 +0000
Finished task: B1: 5s Job on privateConcurQueue withRealDuration: 5 at: 2013-09-19 12:24:22 +0000
Starting task: A2: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:24:22 +0000
Finished task: A2: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:24:27 +0000
Starting task: A3: 5s Job on privateSerialQueue withDuration: 5 at: 2013-09-19 12:24:27 +0000
Finished task: A3: 5s Job on privateSerialQueue withRealDuration: 5 at: 2013-09-19 12:24:32 +0000
Starting task: B2: 5s BARRIER Job on privateConcurQueue withDuration: 5 at: 2013-09-19 12:24:32 +0000
Finished task: B2: 5s BARRIER Job on privateConcurQueue withRealDuration: 5 at: 2013-09-19 12:24:37 +0000
Starting task: B3: 5s Job on privateConcurQueue withDuration: 5 at: 2013-09-19 12:24:37 +0000
Finished task: B3: 5s Job on privateConcurQueue withRealDuration: 5 at: 2013-09-19 12:24:42 +0000
Test run finished at: 2013-09-19 12:24:42 +0000 duration: 25.00404000282288

Not surprisingly, when the barrier block is executing, it is the only block submitted to either queue that is executing. That's because the "unit of protection" is the private concurrent queue of which the private serial queue is a "sub-unit" of. The curious thing that we see here is that task A3, which was submitted to the private serial queue AFTER task B2 was submitted to the private concurrent queue, executes before B2. I'm not sure why that is, but the fundamental unit of protection (i.e. the private concurrent queue) was not violated. Based on that, I conclude that you can't count on the ordering of tasks submitted to two different queues, even if you happen to know that one queue targets the other.

So there you have it. We've proved that dispatch_barrier_async is the same as dispatch_sync on serial and global concurrent queues, just like the documentation said it would be, which left only one operation left to test (a dispatch_barrier_async to the private concurrent queue), and we've illustrated that the unit of protection is preserved in that case including operations submitted to other private queues that target it.

If there's some case you're still not clear on, please comment.

ipmcc
  • 29,581
  • 5
  • 84
  • 147
  • Well I know the case for the serial queue that target to the global concurrent queue but I don't know about the serial queue that target to another custom concurrent queue and then that custom concurrent queue target to the global concurrent queue. I don't know if the barrier block would affect the blocks that are in other custom queue in its queue hierarchy? – Pitiphong Phongpattranont Sep 19 '13 at 04:46
  • 1
    @PitiphongPhongpattranont I have edited my answer to provide you with examples to prove my claims. – ipmcc Sep 19 '13 at 12:49
  • As best I can tell from the docs, that behavior is 'not specified', so while the example here proves that A3 *can* execute before B2, I infer no guarantee that it *will*. – ipmcc Sep 19 '13 at 15:59
  • My question is about the A3 and B2 units in the last example. If the barrier block would block the execution of the block units in another queue. From your result it shows that the barrier block doesn't block the execution of the block units in another queue. Thank you very much for your experiment. It helps me understand the dispatch queue more :) – Pitiphong Phongpattranont Sep 19 '13 at 16:09
  • 1
    When I said 'not specified', that's what I meant: 'isn't mentioned in the docs'. I went and read the libdispatch source and my quick reading of it is that when a block is submitted to a queue, it's not pushed through to the underlying target queue until the submitted-to queue is woken-up. If there's nothing in the submitted-to queue already, the wakeup happens right away, but if there *is*, then the wakeup is deferred until that queue's currently executing block completes. This explains the weird ordering behavior to my satisfaction. But it's not documented, so you shouldn't count on it. – ipmcc Sep 19 '13 at 16:40
  • Sorry, which queue is `submitted-to` queue in this case. PS. Sorry about my English. – Pitiphong Phongpattranont Sep 19 '13 at 17:44
  • 1
    The submitted-to queue would be the serial queue in this case. It really doesn't matter; the behavior isn't documented, so you can't rely on it. If this is an academic interest in understanding libdispatch's internals, then I encourage you to go read the source. At this point, that's all I'm doing, and it's kind of pointless for me to read the source and then try to paraphrase it in stackoverflow comments when you can just go read it yourself. – ipmcc Sep 19 '13 at 17:57
  • I see. Thank you very much. Actually I pull the libdispatch 15 minutes ago and reading it. Any advice on how to walk through the code? I have not so much on this. – Pitiphong Phongpattranont Sep 19 '13 at 18:05
  • 1
    There's not much I'm going to be able to tell you about reading other people's non-trivial code in an SO comment other than "it takes time and perseverance." – ipmcc Sep 19 '13 at 18:14
  • I also post the question in dev forums along with this question too. Apple engineer has answered the question https://devforums.apple.com/message/893866#893866 – Pitiphong Phongpattranont Sep 20 '13 at 08:10
  • 1
    And I see that the answer is exactly as I said. Interesting... :) – ipmcc Sep 20 '13 at 10:35
  • yup, mutual exclusion (for barriers/serial queues) is guaranteed between queues and target queues, but not ordering (and it is unlikely it ever would be given the performance implications). – das Sep 22 '13 at 21:22