Grand Central Dispatch, C++ and inter-thread communication
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 #include 2 #include 3 #include 4 5 #include 6 #include
This is the chunk of data we will be throwing around. Feel free to expand it as you see fit.
1 class Data 2 { 3 public: 4 Data() {} 5 };
We want to be good memory managers and collect more than a few bits of data.
1 typedef std::tr1::shared_ptr DataRef; 2 typedef std::vector 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 // Custom block 2 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 // Generic callback 2 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 // Data bits callback 2 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 // Run loop for the thread we want to talk to 2 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 // Run synchronously 2 3 int RunSync(OurBlock block) 4 { 5 // use an error code to capture the result of the operation 6 7 __block int status = 0; 8 9 // and a semaphore to wait until the operation has completed 10 11 dispatch_semaphore_t sema = dispatch_semaphore_create(0); 12 13 // enqueue the block on the target run loop 14 15 CFRunLoopPerformBlock(__TheOtherRunLoop, kCFRunLoopDefaultMode, ^{ 16 17 // run the block we created previously 18 19 status = block(); 20 21 // and signal that we are done 22 23 dispatch_semaphore_signal(sema); 24 25 }); 26 27 // wake up the target run loop 28 29 CFRunLoopWakeUp(__TheOtherRunLoop); 30 31 // hold on until we are done above 32 33 dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 34 35 // dispose of the semaphore 36 37 dispatch_release(sema); 38 39 // and return the error code 40 41 return status; 42 }
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 // Run asynchronously 2 3 void RunAsync(OurBlock block, OurCallback callback) 4 { 5 // callback will run in the caller thread 6 // and use the caller's run loop 7 8 CFRunLoopRef callbackRunLoop = CFRunLoopGetCurrent(); 9 10 // enqueue a block on the target run loop 11 12 CFRunLoopPerformBlock(__TheOtherRunLoop, kCFRunLoopDefaultMode, ^{ 13 14 // do the work 15 16 int status = block(); 17 18 // enqueue a block on the caller's run loop 19 20 CFRunLoopPerformBlock(callbackRunLoop, kCFRunLoopDefaultMode, ^{ 21 22 // and make the block invoke the callback and pass the status code 23 24 callback(status); 25 26 }); 27 28 // wake up the caller's run loop 29 30 CFRunLoopWakeUp(callbackRunLoop); 31 }); 32 33 // wake up the target run loop 34 35 CFRunLoopWakeUp(__TheOtherRunLoop); 36 }
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 OurBlock MakeCollectBits(BitsOfData& bits) 2 { 3 // allocate block on the heap 4 5 return Block_copy(^{ 6 7 int status = 0; 8 9 cout << "We right here!" << endl; 10 11 // no exceptions can excape the block! 12 13 try 14 { 15 DataRef bit1(new Data); 16 bits.push_back(ref1); 17 } 18 catch (...) 19 { 20 cout << "Unknown exception" << endl; 21 status = -1; 22 } 23 24 return status; 25 }); 26 }
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 // Synchronous gathering of data bits 2 3 int CollectBits(BitsOfData& bits) 4 { 5 // create the block and "capture" bits 6 7 OurBlock block = MakeCollectBits(bits); 8 9 // run the block synchronously 10 11 int status = RunSync(block); 12 13 // release memory allocated to the block 14 15 Block_release(block); 16 17 // we are done 18 19 return status; 20 }
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 // Asynchronous gathering of data bits 2 3 void CollectBitsAsync(BitsOfData& bits, DataBitsCallback callback) 4 { 5 // create a closure as before 6 7 OurBlock block = MakeCollectBits(bits); 8 9 // run asynchronously and trigger a callback created on the fly 10 11 RunAsync(block, ^(int status) { 12 13 // we are inside "OurCallback" here and need to clean up 14 // the bit collection block first and foremost 15 16 Block_release(block); 17 18 // invoke the data bits callback, 19 // giving it what we have collected 20 21 callback(status, bits); 22 }); 23 }
Please feel free to ask any questions!
