I've recently been developing a Mac OS X project in which I was able to utilize the new code blocks feature to write some asynchronous code. They are a very exciting development, and I hope the C standards body adopts them as an official part of the language. They are similar to D's delegates. A good introduction to blocks can be found in Apple's Blocks Programming Topics.
I won't explain what a block is here, other than to say it's like a function. For a very brief introduction to the syntax of blocks, see “Declaring and Using a Block.â€
What I will do is show you how code blocks can simplify inter-thread communication. First I'll present a pre-code block approach to the problem, and then show how, by using code blocks, you can dramatically simplify the code.
Background
The project includes a little USB hardware device, with Mac OS X software to talk to it. I needed to implement a console-like UI, allowing the user to send bytes to the device, and see the bytes coming in from it (in this regard, the device behaves like a serial port). The idea is to simply append whatever bytes are sent by the device to an NSTextView
. To do this, it would be nice if the driver (a user-land driver embedded in the app) would just call my window controller whenever there is data available from the device.
However, there is currently only a blocking read call implemented in the driver.[1] So, I need a separate thread to block on a read call, and when data becomes available, to send my window controller a message to handle it. Problem is, the window controller needs to then interact with NSTextView
, which should only be done on the main thread. The traditional solution in a situation like this is to use -[performSelectorOnMainThread:]
.
Traditional Approach
The window controller mentioned above is really just the app delegate (it's currently a very simple app). The app delegate's declaration looks something like this. For simplicity's sake, I've left out a lot, but for our purposes, assume that the user presses a button which calls -[openTerminal:]
, which starts the balls rolling.
[cpp 1="CallbackReadOp;" 2="@class" 3="Device;" 4="@interface" 5="AppDelegate" 6=":" 7="NSObject" 8="<NSApplicationDelegate>" 9="{" 10="NSWindow*" 11="mWindow;" 12="NSTextView*" 13="mConsole;" 14="Device*" 15="mDevice;" 16="CallbackReadOp*" 17="mReadOp;" 18="}" 19="@property" 20="(retain)" 21="IBOutlet" 22="NSWindow*" 23="window;" 24="@property" 25="(retain)" 26="IBOutlet" 27="NSTextView*" 28="console;" 29="@property" 30="(retain)" 31="Device*" 32="device;" 33="-" 34="(IBAction)" 35="openTerminal:" 36="(id)" 37="inSender;" 38="@end" language="@class"][/cpp]
Pretty straightforward, right? We've got some references to the UI, a reference to our device, and the action to start the read-append operation.
The implementation looks like this:
[cpp 1=""AppDelegate.h"" 2="#import" 3=""CallbackReadOp.h"" 4="@interface" 5="AppDelegate()" 6="-" 7="(void)" 8="readData:" 9="(NSData*)" 10="inData;" 11="-" 12="(void)" 13="appendToConsole:" 14="(NSString*)" 15="inString;" 16="@end" 17="@implementation" 18="AppDelegate" 19="-" 20="(IBAction)" 21="openTerminal:" 22="(id)" 23="inSender" 24="{" 25="[self.device" 26="26="open"];" 27="//" 28="Create" 29="an" 30="operation" 31="to" 32="loop" 33="reading" 34="from" 35="the" 36="board," 37="//" 38="calling" 39="us" 40="back" 41="when" 42="there's" 43="data" 44="available…" mreadop="[[CallbackReadOp" 45="alloc]" 46="init];" 47="mReadOp.device" 48="=" 49="self.device;" 50="mReadOp.target" 51="=" 52="self;" 53="mReadOp.selector" 54="=" 55="@selector(readData:);" 56="[mOpQueue" 57="addOperation:" 58="mReadOp];" 59="}" 60="-" 61="(void)" 62="readData:" 63="(NSData*)" 64="inData" 65="{" 66="NSString*" s="[[NSString" 67="alloc]" 68="initWithData:" 69="data" 70="encoding:" 71="NSUTF8StringEncoding];" 72="if" 73="(s" 74="!=" 75="nil)" 76="{" 77="[self" 78="appendToConsole:" 79="s];" 80="}" 81="}" 82="#pragma" 83="mark" 84="-" 85="#pragma" 86="mark" 87="•" 88="Attributes" 89="@synthesize" window="mWindow;" 90="@synthesize" console="mConsole;" 91="@synthesize" device="mDevice;" 92="@end" language="#import"][/cpp]
But wait, there's more! We need to implement CallbackReadOp
. Here's its declaration:
[cpp 1="Device;" 2="@class" 3="CallbackReadOp" 4=":" 5="NSOperation" 6="{" 7="Device*" 8="mDevice;" 9="NSObject*" 10="mTarget;" 11="SEL" 12="mSelector;" 13="}" 14="@property" 15="(retain)" 16="Device*" 17="device;" 18="@property" 19="(retain)" 20="NSObject*" 21="target;" 22="@property" 23="(assign)" 24="SEL" 25="selector;" 26="@property" 27="(assign)" 28="bool" 29="reading;" 30="@end" language="@class"][/cpp]
The implementation looks like this:
[cpp 1=""CallbackReadOp.h"" 2="@implementation" 3="CallbackReadOp" 4="-" 5="(void)" 6="main" 7="{" 8="while" 9="(self.reading)" 10="{" 11="uint8_t" 12="12="buf[512"];" 13="uint32_t" numbytes="[self.device" 14="read:" 15="buf" 16="length:" 17="512" 18="timeout:" 19="250];" 20="if" 21="(numBytes" 22=">" 23="0)" 24="{" 25="NSData*" data="[NSData" 26="dataWithBytes:" 27="buf" 28="length:" 29="numBytes];" 30="[self.target" 31="performSelectorOnMainThread:" 32="self.selector" 33="withObject:" 34="data" 35="waitUntilDone:" 36="true];" 37="}" 38="}" 39="}" 40="#pragma" 41="mark" 42="-" 43="#pragma" 44="mark" 45="•" 46="Attributes" 47="@synthesize" device="mDevice;" 48="@synthesize" target="mTarget;" 49="@synthesize" selector="mSelector;" 50="@synthesize" reading="mReading;" 51="@end" language="#import"][/cpp]
This should be fairly self-explanatory. When this custom NSOperation
subclass is added to the NSOperationQueue
in the app delegate's -[openTerminal:]
method, its -[main]
is invoked. It sits in a loop, waiting for data to be available, and sends the app delegate -[performSelectorOnMainThread:withObject:waitUntilDone:]
, passing the NSData
object it creates for the app delegate to handle.[2] If the user closes the console, the app delegate can set the reading
property to false
, and the loop will exit.
-[appendConsole:]
takes care of presenting the data to the user in the app's window. The details of that aren't relevant here, so I've left it out.
So, all of that wasn't too bad. But it requires two methods in the app delegate, and a custom subclass of NSOperation
. Now let's see how it works when using code blocks.
With Code Blocks
With code blocks, we can get rid of the custom NSOperation
subclass altogether, and get rid of one of the two methods in the app delegate. All we need is a new implementation of -[openTerminal:]
:
[cpp 1="(IBAction)" 2="openTerminal:" 3="(id)" 4="inSender" 5="{" 6="[self.device" 7="7="open"];" 8="//" 9="Create" 10="an" 11="operation" 12="to" 13="loop" 14="reading" 15="from" 16="the" 17="board," 18="//" 19="appending" 20="to" 21="the" 22="console…" 23="NSBlockOperation*" op="[NSBlockOperation" 24="blockOperationWithBlock:" 25="^{" 26="NSAssert([NSOperationQueue" 27="currentQueue]" 28="!=" 29="[NSOperationQueue" 30="mainQueue]," 31=""We" 32="should" 33="be" 34="running" 35="on" 36="a" 37="different" 38="queue!");" 39="while" 40="(self.reading)" 41="{" 42="uint8_t" 43="buf[512];" 44="uint32_t" numbytes="[self.device" 45="read:" 46="buf" 47="length:" 48="512" 49="timeout:" 50="250];" 51="if" 52="(numBytes" 53=">" 54="0)" 55="{" 56="NSData*" data="[NSData" 57="dataWithBytes:" 58="buf" 59="length:" 60="numBytes];" 61="//" 62="We've" 63="got" 64="some" 65="data," 66="now" 67="append" 68="it" 69="to" 70="the" 71="output" 72="view…" 73="NSOperation*" callbackop="[NSBlockOperation" 74="blockOperationWithBlock:" 75="^{" 76="NSString*" s="[[NSString" 77="alloc]" 78="initWithData:" 79="data" 80="encoding:" 81="NSUTF8StringEncoding];" 82="if" 83="(s" 84="!=" 85="nil)" 86="{" 87="[self" 88="appendToConsole:" 89="s];" 90="}" 91="else" 92="{" 93="NSLog(@"Got" 94="%lu" 95="bytes," 96="but" 97="no" 98="valid" 99="UTF-8" 100="string"," 101="data.length);" 102="}" 103="}];" 104="[[NSOperationQueue" 105="mainQueue]" 106="addOperation:" 107="callbackOp];" 108="[callbackOp" 109="waitUntilFinished];" 110="}" 111="}" 112="}];" 113="[mOpQueue" 114="addOperation:" 115="op];" 116="}" language="-"][/cpp]
What's happening here? There are two (nested) anonymous code blocks, each delimited by ^{
and }
. This notation is a shorthand for a block that takes no parameters and returns void
.
The Outer Block
First, line 9 creates an NSBlockOperation
, and provides a block definition inline (the definition ends on line 43). This block is essentially the same as the code in CallbackReadOp
: It loops over a device read call, creating NSData
objects every time it gets some data. (It also asserts, on line 11, that it is in fact executing on a thread that's not the main thread.)
Keep in mind that the code in this code block will not execute right now (that is, right after the call to +[NSBlockOperation blockOperationWithBlock:]
). It is simply being defined, and a reference to it is passed as the sole parameter to +[NSBlockOperation blockOperationWithBlock:]
. More on that later.
On line 17, the code block references self.device
. This is where the beauty and power of blocks becomes apparent: code blocks have access to all the variables in scope at the place they are defined. In this case, the blocks are defined in the scope of the AppDelegate
method -[openTerminal:]
. So they can access anything “regular†code in -[openTerminal:]
can access, even though it will actually execute on another thread, after -[openTerminal:]
has exited!
Note: It's important to understand that this access isn't completely without restriction; you still have to take care to synchronize access to shared variables just like you would with conventional multithreaded code, and remember that the block will hold strong references to garbage-collected objects (see the “Garbage Collection†section below). But what you don't have to do is squirrel away references to things like we did above with the CallbackReadOp
class.
For more details on the subtleties of variable access from within code blocks, see “Blocks and Variablesâ€.
Now take a look at line 45. Here we take the NSBlockOperation
object op
that we just created, and add it to mOpQueue
, an instance of NSOperationQueue
that we created earlier in the application's execution. At this point, our code block is scheduled to execute on a different thread, and -[openTerminal:]
exits.
Eventually, that new thread gets scheduled, and the outer block we just created executes.
The Inner Block
So we have an outer code block executing on a thread other than the main thread, looping over a read call and passing data back to the main thread. Let's take a look at how it actually accomplishes that second part.
In the traditional approach, CallbackReadOp
calls -[performSelectorOnMainThread:withObject:waitUntilDone:]
, calling the app delegate's -[readData:]
and passing it the NSData
object it created as a result of the device read. -[readData:]
converts that to a string and calls -[appendToConsole:]
to display it.
While we could certainly do that in the outer code block above, we can get rid of the -[readData:]
method altogether and just call -[appendToConsole:]
directly. Remember, a code block has access to everything that was in scope at the place it was defined, even when executing later. The only thing here, though, is that we need to call -[appendToConsole:]
on the main thread. So we just do what we did with the outer block, but this time schedule it to run on the main thread, via +[NSOperationQueue mainQueue]
.
Everything from line 26 to line 36 will execute on the main thread. The separate thread conducting the device read will block until the inner code block has completed execution, thanks to the call to -[waitUntilFinished]
.[3]
Improving the Code
There is at least one small improvement we can make to this code, and that is to move the creation of the NSAttributedString
to the outer block. Doing so will push more work into the other thread, reducing the burden on the main thread an improving UI responsiveness (on multi-core machines, at least). In this case it's not likely to make a noticable difference, but it's important to think in these terms.
Without code blocks, making such a trivial change would be more burdensome, because you'd have to change code in two different classes, and change a method signature. With the code blocks we've structured here, it's mostly just rearranging a few lines:
[cpp 1="(IBAction)" 2="openTerminal:" 3="(id)" 4="inSender" 5="{" 6="[self.device" 7="7="open"];" 8="//" 9="Create" 10="an" 11="operation" 12="to" 13="loop" 14="reading" 15="from" 16="the" 17="board," 18="//" 19="appending" 20="to" 21="the" 22="console…" 23="NSBlockOperation*" op="[NSBlockOperation" 24="blockOperationWithBlock:" 25="^{" 26="NSAssert([NSOperationQueue" 27="currentQueue]" 28="!=" 29="[NSOperationQueue" 30="mainQueue]," 31=""We" 32="should" 33="be" 34="running" 35="on" 36="a" 37="different" 38="queue!");" 39="while" 40="(self.reading)" 41="{" 42="uint8_t" 43="buf[512];" 44="uint32_t" numbytes="[self.device" 45="read:" 46="buf" 47="length:" 48="512" 49="timeout:" 50="250];" 51="if" 52="(numBytes" 53=">" 54="0)" 55="{" 56="NSData*" data="[NSData" 57="dataWithBytes:" 58="buf" 59="length:" 60="numBytes];" 61="//" 62="Convert" 63="the" 64="data" 65="to" 66="a" 67="string…" 68="NSString*" s="[[NSString" 69="alloc]" 70="initWithData:" 71="data" 72="encoding:" 73="NSUTF8StringEncoding];" 74="if" 75="(s" 76="==" 77="nil)" 78="{" 79="NSLog(@"Got" 80="%lu" 81="bytes," 82="but" 83="no" 84="valid" 85="UTF-8" 86="string"," 87="data.length);" 88="continue;" 89="}" 90="//" 91="We've" 92="got" 93="some" 94="data," 95="now" 96="append" 97="it" 98="to" 99="the" 100="output" 101="view…" 102="NSOperation*" callbackop="[NSBlockOperation" 103="blockOperationWithBlock:" 104="^{" 105="[self" 106="appendToConsole:" 107="s];" 108="}];" 109="[[NSOperationQueue" 110="mainQueue]" 111="addOperation:" 112="callbackOp];" 113="[callbackOp" 114="waitUntilFinished];" 115="}" 116="}" 117="}];" 118="[mOpQueue" 119="addOperation:" 120="op];" 121="}" language="-"][/cpp]
Garbage Collection
As you may have surmised by looking at the code, this app is garbage collected. That is, the code doesn't use retain
and release
to manage objects. This can obscure the fact that code blocks strongly reference the objects “captured†by them.[4]
Here, the blocks explicitly reference self
and self.device
, which will cause both of those objects to persist for as long as the blocks persist. If you were to directly reference an ivar, that would implicitly reference self, so again, both would continue to live for the life of the block. In this case, the app delegate and the device are both objects that always exist longer than the span of a user's console session, so this is acceptable.
The outer block defined above can live for a very long time (for as long as the user has the console window open). Blocking operations within a block can also cause a block to live for an extended period of time. If you're not careful, this can cause the worker thread count to increase dramatically, a condition you almost always want to avoid.
Final Thoughts
Code blocks (and the NSOperation
classes to support using them) provide a clean and concise way of writing asynchronous and multi-threaded code. For this app, we got rid of an entire class definition (the CallbackReadOp
), and a method in the app delegate. We did have to add an atomically-access reading
property to the app delegate (not shown). Much easier to write, and to understand (once you understand code blocks).
Most often you will write small, anonymous blocks of code like those above and pass them to other functions for execution. Being able to access the variables in scope at the point of definition makes it very easy to write complex code, just don't forget to synchronize access to shared variables.
For additional information, and if you have Apple Developer access, check out A Short Practical Guide to Blocks.
Acknowledgements
Special thanks to Joar Wingfors of Apple, Inc. for technical review and other suggestions.
Notes