Tenerife Skunkworks

Trading and technology

Grand Central Dispatch

Now that Grand Central Dispatch is part of iPhone OS 4.0, I would like to showcase its unique benefits for inter-thread communication.

Passing data from one thread to another is normally a royal pain on the Mac, something involving the use of Mach ports, something I’d rather not talk about. Ever! Assuming that your threads use Core Foundation or Cocoa, your threads have a run loop and you can shuffle bits of data back and forth with no trouble whatsoever. You can even do it in C++.

We will look at running a code block in another thread and waiting for it to finish (synchronous execution), as well as running a block in another thread and having it run a callback that we supply (asynchronous execution).

Let’s grab a few required header files and get started!

1
2
3
4
5
6
#include <tr1/memory>
#include <iostream>
#include <vector>

#include <Block.h>
#include <dispatch/dispatch.h>

This is the chunk of data we will be throwing around. Feel free to expand it as you see fit.

1
2
3
4
5
class Data
{
public:
  Data() {}
};

We want to be good memory managers and collect more than a few bits of data.

1
2
typedef std::tr1::shared_ptr<Data> DataRef;
typedef std::vector<DataRef> BitsOfData;

A block type is different from a function pointer in that it uses a ‘hat’ (^) instead of a ‘star’ (*). We want to know how our operation went so we return an integer error code.

1
2
// Custom block
typedef int (^OurBlock)(void);

We need a callback type for asynchronous execution. Once or block runs on some other thread, it will invoke our callback and pass the error code from the operation (block) that we just executed. Note that our callback is also a block.

1
2
// Generic callback     
typedef void (^OurCallback)(int status);

We are using C++ so let’s make another callback, one that takes both the status from the block we just executed and the bits of data we collected in the process.

1
2
// Data bits callback    
typedef void (^DataBitsCallback)(int status, BitsOfData& bits);

We will assume that there’s a single thread that we want to talk to, running somewhere, all lonely. There is nothing preventing you from passing the run loop as an argument, though. You do need to have a reference to the run loop, though, no way around it!

1
2
// Run loop for the thread we want to talk to    
extern CFRunLoopRef __TheOtherRunLoop;

Synchronous execution of a block uses a semaphore for synchronization. We want to tell the other thread to run the block for us and we want to sit quietly until we are told that the block has finished executing.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// Run synchronously

int RunSync(OurBlock block)
{
  // use an error code to capture the result of the operation

  __block int status = 0;

  // and a semaphore to wait until the operation has completed

  dispatch_semaphore_t sema = dispatch_semaphore_create(0);

  // enqueue the block on the target run loop

  CFRunLoopPerformBlock(__TheOtherRunLoop, kCFRunLoopDefaultMode, ^{

    // run the block we created previously

    status = block();

    // and signal that we are done

    dispatch_semaphore_signal(sema);

  });

  // wake up the target run loop 

  CFRunLoopWakeUp(__TheOtherRunLoop);

  // hold on until we are done above

  dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER);

  // dispose of the semaphore 

  dispatch_release(sema);

  // and return the error code

  return status;
}

Note that we both wait forever on the semaphore and give the run loop of the target thread a gentle bump to remind it that there’s work to do.

Running a block asynchronously is conceptually simple: we tell the other thread to run the block, the other thread tells us to run the callback once it’s done. There are no semaphores and no waiting.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// Run asynchronously

void RunAsync(OurBlock block, OurCallback callback)
{
  // callback will run in the caller thread 
  // and use the caller's run loop 

  CFRunLoopRef callbackRunLoop = CFRunLoopGetCurrent();

  // enqueue a block on the target run loop 

  CFRunLoopPerformBlock(__TheOtherRunLoop, kCFRunLoopDefaultMode, ^{

    // do the work

    int status = block();

    // enqueue a block on the caller's run loop 

    CFRunLoopPerformBlock(callbackRunLoop, kCFRunLoopDefaultMode, ^{

      // and make the block invoke the callback and pass the status code

      callback(status);

    });

    // wake up the caller's run loop 

    CFRunLoopWakeUp(callbackRunLoop);
  });

  // wake up the target run loop

  CFRunLoopWakeUp(__TheOtherRunLoop);
}

This is what a sample operation (block) look like. Note that we need to allocate the block on the heap rather than the stack (default). This is where Block_copy comes in. We want nothing more than to collect a few bits of data into a vector, making sure no exceptions excape the block. We will not need to change the block code for synchronous and asynchronous execution. Hurray for code reuse!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
OurBlock MakeCollectBits(BitsOfData& bits)
{
  // allocate block on the heap

  return Block_copy(^{

    int status = 0;

    cout << "We right here!" << endl;

    // no exceptions can excape the block!

    try
    {
      DataRef bit1(new Data);
      bits.push_back(ref1);
    }
    catch (...)
    {
      cout << "Unknown exception" << endl;
      status = -1;
    }

    return status;
  });
}

Here is what synchronous collection of bits looks like. Note that we need to use deallocate a block and release its resources once we no longer need it. This is where Block_release comes in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// Synchronous gathering of data bits

int CollectBits(BitsOfData& bits)
{
  // create the block and "capture" bits 

  OurBlock block = MakeCollectBits(bits);

  // run the block synchronously

  int status = RunSync(block);

  // release memory allocated to the block

  Block_release(block);

  // we are done

  return status;
}

Running asynchronously requires us to run a given callback after we are done. We also must release the block we were asked to run. We work around this by creating a callback on the fly that first releases the block and then runs the callback we were given.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Asynchronous gathering of data bits

void CollectBitsAsync(BitsOfData& bits, DataBitsCallback callback)
{
  // create a closure as before

  OurBlock block = MakeCollectBits(bits);

  // run asynchronously and trigger a callback created on the fly

  RunAsync(block, ^(int status) {

    // we are inside "OurCallback" here and need to clean up 
    // the bit collection block first and foremost

    Block_release(block);

    // invoke the data bits callback, 
    // giving it what we have collected

    callback(status, bits);
  });
}

Let me know if you have any questions!

Comments