Video hosted by Apple at devstreaming-cdn.apple.com

Configure player

Close

WWDC Index does not host video files

If you have access to video files, you can configure a URL pattern to be used in a video player.

URL pattern

preview

Use any of these variables in your URL pattern, the pattern is stored in your browsers' local storage.

$id
ID of session: wwdc2011-210
$eventId
ID of event: wwdc2011
$eventContentId
ID of session without event part: 210
$eventShortId
Shortened ID of event: wwdc11
$year
Year of session: 2011
$extension
Extension of original filename: m4v
$filenameAlmostEvery
Filename from "(Almost) Every..." gist: [2011] [Session 210] Mastering G...

WWDC11 • Session 210

Mastering Grand Central Dispatch

Core OS • iOS, OS X • 42:47

Grand Central Dispatch is a powerful technology on Mac OS X and iOS that simplifies multi-threaded development. Learn about the latest advancements in Grand Central Dispatch, discover how you can adopt GCD in your own application to increase performance, and gain the real world knowledge you need take your app to the next level.

Speaker: Daniel Steffen

Unlisted on Apple Developer site

Downloads from Apple

HD Video (132.7 MB)

Transcript

This transcript was generated using Whisper, it may have transcription errors.

Good morning. Welcome to Mastering Grand Central Dispatch. My name's Daniel Steffen. I'm the engineer responsible for GCD on the CoreOS team. And as you know, we've introduced Grand Central Dispatch in Mac OS X Snow Leopard, and we're pleased to bring it to iOS 4 last year here at the conference. It has been very widely adopted in both your apps and in Apple's frameworks. And indeed, as you may have seen in other sessions, this week we are introducing a large number of new APIs in Lion and iOS 5 that coordinate or relate to GCD in some way. So it's becoming an ever more important core technology for you to understand if you do any kind of asynchronous or concurrent programming on our platform. It's worth pointing out that the GCD API is identical across our platforms. So you can use the same functionality on both Mac OS X and iOS. And GCD will scale across our hardware from a single-core iPhone all the way up to a 24 core Mac Pro. So today we're going to give a very brief introduction to GCD. There was a more in-depth introductory session yesterday that I hope you managed to catch if you're new to the technology. And we'll look at what is new in GCD on Mac OS X line and iOS 5 and give some advanced usage of GCD API. So what is GCD? At the very core of GCD are blocks and queues. Why blocks? These are very useful in asynchronous programming to encapsulate units of work.

along with the associated state. So in this example here, we create a block, and it encapsulates some piece of work that we want to do asynchronously, and the block captures the state that it needs from the surrounding stack. For instance, here, it will retain the object and copy the integer argument.

So once that is done, we pass that block to a function that will execute the block asynchronously, And now we can safely change the integer or even release the Objective-C object without affecting the block. Queues are the central concept in GCD. They are just a very lightweight list of blocks, really, and it is very cheap to create queues, so it's fine to have many queues in your application. The enqueue and dequeue operations of our queues are FIFO. So blocks get dequeued and executed in the order that they were enqueued.

And importantly, the enqueue operation itself is atomic. So it is safe for multiple threads to enqueue onto a single queue at the same time. These properties, along with the fact that serial queues execute blocks one at the time, make these queues useful for synchronization and serialization purposes in your application. We also provide concurrent queues. These execute multiple blocks at the same time. There's still dequeue blocks in order, but because they can execute concurrently, these blocks may then complete out of order. Another way to get concurrency, which is it is to use multiple serial queues. That works because all of our queues execute concurrently with respect to other queues. So this is an example of that in action. Here we have two serial queues with some blocks enqueued, and we have a worker thread on one CPU that starts to dequeue some blocks and execute them. If a second worker thread can come along, it can dequeue from the other queue, and as you can see, we have some concurrency where there was overlap between blocks from the two queues. Similarly, with a concurrent queue, Here we have two worker threads that dequeue items from the one concurrent queue and execute them concurrently. Very briefly, the API to submit blocks to queues is dispatch async and dispatch sync.

Dispatch async just enqueues a block and goes along, whereas dispatch sync will enqueue the block and wait until that block is actually executed on the queue. You can also delay the submission of blocks to a queue by calling the dispatch after API can currently submit and execute a single block many times to a queue by calling the dispatch apply API.

We also allow you to suspend and resume execution of a dispatch queue by calling the dispatch suspend and resume APIs. This is an asynchronous operation suspension, so it will take effect at the earliest possible opportunity on a queue, meaning it won't interrupt any ongoing blocks that are already executing, but as soon as the can't block is finished executing, the queue will stop execution. If you want to make this process deterministic, the way to do that is to call dispatch suspend from a block that is actually executing on the queue itself. This way you know that your can't block is the last one that will execute before suspension takes effect.

And our queues are reference counted, and you maintain that reference count with dispatch retain and release. A couple of pitfalls to go over with dispatch queues. They are really intended for flow control in your application and not as a general purpose data structure. If you have a lot of work that you may need to execute in your application asynchronously, it is better to keep that work in an actual data structure until you're sure that you really have to execute the work and only then submit it to a queue. In particular, this is the case because once you submit a block to a queue, there's no way to cancel that. The block, we guarantee that that block will execute. Also, as with any multi-threaded programming, be very careful when you use any synchronous API. GCD does not protect you from the possibility of deadlock. For instance, if you have used our dispatch sync API, it is very possible that you can deadlock yourself by waiting for a queue that is waiting on the queue that you're on in turn. Also, if you use our recommended pattern to apply or use a serial queue for each subsystem in your application or each resource that you need to -- each shared resource that you need to protect. If you block the queue that protects that resource or that subsystem, you will block the whole application's access to that. So any kind of synchronous API on those queues would be bad. We have a couple of queue types. The global queues that you get with the dispatch get global queue API.

Passing in a priority. These are concurrent queues. They execute multiple blocks at the same time. And they are global across your whole application. The main queue is another global object. This is a serial queue that cooperates with the main run loop of your application. This is typically where you would execute blocks that interact with the UI subsystem.

So this is one of the queues where it is very important to not run any blocking synchronous API, of course, because you would prevent event processing in the app if you do that. Serial queues you can create yourself by calling the dispatch queue create API and passing in a label. New in Lion and iOS 5 are concurrent queues. Thank you.

Like the serial queues, you create these with the dispatch queue create API, and you pass in the dispatch queue concurrent parameter. These, like the global concurrent queues, will execute blocks, multiple blocks at the same time, but they can be suspended like a serial queue. And a new concept, they support barrier blocks. What are barrier blocks? These are blocks that represent synchronization points in an otherwise concurrent queue. So they will not run until all the blocks that were submitted before the barrier block have completed. And while they're executing, nothing else can run on the queue.

Any blocks that were submitted later will wait until that barrier block has executed. You submit barrier blocks by calling the dispatch barrier async or dispatch barrier sync APIs. These are exactly like dispatch async and dispatch sync, except that they mark the block as a barrier block. Note that these barrier blocks don't do anything special on the global concurrent queues, because the global concurrent queues are a shared resource across the whole application.

We don't want anybody to be able to monopolize those queues. So here we have the same animation as before, except that we have one of these concurrent, one of these barrier blocks in the middle of one of these new concurrent queues, submitted by the dispatch barrier async API. And once threads start dequeuing blocks here, The yellow barrier block will not start executing until all the existing work on the queue has finished running. Then while the block, the barrier block, is executing, nothing else can run. And once it has finished, normal concurrent execution can continue. One important use case for these new concurrent queues are to implement efficient read or write schemes with GCD.

So this you would use if you have a shared data structure or something of that sort that can admit many concurrent readers or a single writer. And you would use dispatch sync to a concurrent queue that protects that resource to implement the reader and dispatch barrier async to implement the writer.

If there's no writers on the concurrent queue, the dispatch sync is actually a very cheap check. And this will be a very efficient way to implement the reader-writer lock. Of course, if there is a write in progress, the readers will be enqueued on the queue and wait until the write has completed.

And conversely, while there are reads in progress, the writer will be enqueued and wait until it can have exclusive access to the resource. So let's look at an example of that in code. Here we are implementing some type of container object similar to an NSMutable array with a getter and a setter. but we want to admit concurrent getters while still being able to safely mutate the object with a setter. So implement the object at index getter by just passing through to our internal NSMutableArray instance variable, but to make this safe against mutation, concurrent mutation, we wrap this into a dispatch sync block onto a concurrent queue instance variable. So this will make sure that while many of these can occur long as there is no write occurring. To implement the writer, we just pass through the setter inset object at index to our instance variable and wrap that into a dispatch barrier async which will make sure that this mutation is executing exclusively while no other readers as well as no writers are ongoing. Our next topic is target queues. Target queues are in GCDR where the a dequeue operation for a queue is being executed. So all of our queues have target queues, except for the global ones. The global queues are at the bottom of the target queue hierarchy, and it is indeed the priority of the global queue that determines the scheduling priority of the worker threads that execute the work on these queues, and determines the dequeue priority if there's more work available than can be processed at any given time.

So we currently have three priority levels, low, default, and high. And if you create a serial queue, by default, it will come with the default priority global queue set as its target queue. Then you can submit some back to that queue and later on potentially change its target queue, say, to the low priority queue. Of course, you can also, when you create a queue, immediately change the target queue here. We've set it to be the high priority queue.

So items that you submit to the serial key on the right will now execute on a thread that runs at high priority. New in IS5 and line, we're adding a fourth priority level, the background priority. And of course, with the new concurrent queues, you can also set the target queue. So here we've set it to be this new background priority global queue.

More on that, the background priority global queue you get by passing the dispatch queue priority background constant to dispatch get global queue. This will, blocks that you submit to this queue will run on threads that run at the lowest possible scheduling priority in your process. And any I/O that you do from that thread will be throttled I/O system-wide that will take a backseat to any normal I/O that's ongoing. So this queue is very useful if you have any kind of idle time work or scanning of the disk or indexing, anything like that that should not interfere with other more high priority work on the system. As mentioned, target queues can form a whole hierarchy.

And the way to set that up is by calling dispatch set target queue. This in itself is an asynchronous operation on the queue. So any work that is already enqueued on the queue and executing will not be affected by this changing of target queue. It's only once all the work that was submitted ahead of the call to dispatch the target queue has finished that the target queue will change and affect any future blocks. And we support arbitrary deep hierarchies, if that is useful to you, but not loops. There's a couple of useful, somewhat non-trivial idioms with target queues that we'd like to go through now. One is that you can synchronize with a serial queue by setting the target queue of your queue to be that serial queue. Synchronized meaning that there will be a block executing either on your queue or on the serial queue, but not on both at the same time. However, setting that up does not implicitly order, impose any ordering between the two queues. So if you submit blocks to both queues in a certain order, that doesn't mean that they will be dequeued in that order.

So here we have, as an example, two serial queues that come in the default state with the target queue set to the default priority global queue. And you can synchronize them by changing the target queue of one to be the other serial queue. Now, again, this means that there will be a block executing either on one or the other of these two serial queues, but not on both at the same time.

Of course, you can do the same with one of our new concurrent queues. And what that does is that it actually makes the concurrent queue serial. The reason for that is that, remember I said, target queues are where the dequeue operation for a queue is executed. And because here the target queue of a concurrent queue is serial, the dequeue operation becomes serialized. So we cannot run more than one block at the same time, even though the queue was concurrent. So one use case for that is if you can't use a read or write a lock, you could promote it to an exclusive lock by doing this.

Another example is for target queue use is if you're writing a library and you have an API that takes a queue that the user gives you where you should invoke callbacks. And you have a constraint that you don't want to invoke two callbacks at the same time. But you don't know whether the queue that the user gave you is concurrent or not. So just dispatch asyncing to that queue isn't going to work because you could have multiple callbacks executing concurrently.

One way to impose an ordering there is to create your own internal serial queue and set it up so that the user's queue is the queue that they gave you. Sorry, that the user's queue is the target queue of your internal serial queue. And if you now submit WAG to the serial queue, it will have an ordering, but it will still execute in the context of the user-supplied callback queue. Another question that we get asked sometimes is, how can I enqueue at the front of a serial queue? This is typically in the context where you use a serial queue as a means of protecting access to a shared resource, but you have some items, so you enqueue some work that access that shared resource, but you have some work item that is of higher importance and doesn't want to wait for all the already enqueued work to complete, so you'd like to jump ahead in the queue. You can't do that with a single serial queue in GCD because our queues are strictly FIFO, but you can use the target queue mechanism to get a queue suspension to achieve that. Let's look straight at the code of how this works. We set up two serial queues, one for the low-priority items and one for these high-priority items that would like to jump ahead and set the target queue relationship up so that the high-priority queue is the target queue of the low-priority one. Then we just submit our normal low-priority work to the low-priority queue, and it will have exclusive access to the shared resource.

and if one of these high-priority items comes along, we suspend the low-priority queue, so this will interrupt as soon as the can't work item that is executing has finished, will interrupt processing of the low-priority items. Now we can async to the high-priority queue and do our high-priority work, and once that is done, we just resume from inside that block the low-priority work and that can then continue. So looking at this in an animation, here we have one thread enqueuing some work onto our low-priority queue. The second thread comes along and dequeues it and runs it. But now we have this high-priority work item that would like to jump ahead of the work that's already enqueued here. So we implement that algorithm. We suspend the low-priority queue and enqueue ourselves onto the target queue, the high-priority queue. And now as soon as the current executing work item is finished, this can be dequeued and run. If any other work gets enqueued on the low priority queue, of course, that will not execute because that queue is now suspended.

As soon as that red high-priority item has finished, we resume the low-priority queue, and the dequeue operation can continue as normal. So now we've managed to jump ahead of the queue. Our next topic is queue-specific data. This is a new feature in iOS 5 and Lion. This allows you to associate arbitrary key-value storage to a queue. We already had dispatch set context and get context, but that was a single pointer and wasn't really sufficient if you were sharing your queues among multiple subsystems in your application.

So here we allow you with the dispatch queue get specific and dispatch queue set specific APIs to set key value storage for any queue. And we use keys that we just compare as pointers. So typically we recommend that you use the address of a static variable in your subsystem that will be a unique pointer to your subsystem. Don't just pass in a string because that may not be a unique pointer.

And for the setters, you pass in a value and a destructor function. That destructor function is called when the value is unset by another call to dispatch queue set specific, or when the queue is destroyed. And a new capability we make available for you with this is that you can get the current value for a given key when you're running in a block on a queue with the dispatch get specific API. You just pass in the key, and this will return the value that is set on the currently executing queue, Or importantly, this is aware of the target queue hierarchy. If this count queue doesn't have a value set, it will go and look in the target queue, does this have the value set for this key, and so on until it reaches the bottom of the hierarchy.

So to illustrate this here, we have three queues set up in a target queue relationship, and we set a value for a key, value 1 set on the QA and value 2 set on QB, but no value for this key set on QC. If you now run a block on QB and call dispatch get specific there, you will see value 2. But if you run it on QC, because it doesn't have its own value, it will see the value from the target queue, value 1. Another new API set that we are introducing in iOS 5 and line is dispatch data. This is a facility that provides container objects to you for multiple discontinuous memory buffers. And importantly, these container objects, as well as the represented buffers, must be immutable.

So the container objects, once you've created them, you can no longer change them, and any buffers that you pass to us to be represented by dispatch data must not be changed once you've given them to us. And the central goal of the whole API set is to avoid copying buffers as much as possible.

So the way you create one of these dispatch data objects is to call dispatch data create. And you pass in a buffer and a size, and it will return you a dispatch data T object that you manage with dispatch retain release, like all the other dispatch objects. So we represent that graphically here with a a blue memory buffer represented by a red dispatch data object. And because the buffers are required to be immutable, we can now have multiple dispatch data objects that reference the same buffer. And, of course, only once all the data objects go away will the buffer be deallocated. The way that deallocation happens is via this destructor block that you give us as the last parameter in dispatch data create. And there's a couple possibilities for that. you can pass the dispatch data destructor default constant. This will tell us you should make a snapshot of this data. I can't really guarantee that this buffer will not change for the lifetime of the data object, so we make a copy and then manage the lifetime of that copy ourselves. So, of course, if you can avoid it, this is not the preferred choice because it involves making a copy.

If you just have a buffer allocated in malloc, you pass dispatch data destructor free, which is essentially cool to giving us a block that will call free on the buffer, but is more efficient. And if you have some custom buffer that is maybe wrapped into another object, it just passes a block that then does whatever the allocation break is required. So in this example, for instance, we've passed in the buffer underlying a CFData object to the creation API, and for the destructor, we just release the CFData. You can also create dispatch data objects from other objects, from other data objects. For instance, via concatenation with the dispatch data, create concat API. This takes two existing data objects and makes a new object from them that now represents the two memory buffers concatenated together. And of course, again, because of counting of references, when these original source objects go away, the memory buffers will stick around until the concatenated object also goes away. You can also make a sub-range of an existing data object if you're only interested in a part of the data. by calling the dispatch data create sub-range API. So here, as an example, we have a data object that represents four discontinuous memory buffers, and we're really only interested in the piece between the two light blue arrows. So we call the dispatch data create sub-range API with this range here, which makes a new object that only represents those two pieces. And if you now get rid of the source data object, we can deallocate the two buffers that are no longer required. So if you do any kind of incremental processing of data in your application, this can be a very easy way to get rid of data on an ongoing basis. And again, of course, when that goes away, the remaining buffers get deallocated.

Once you have a dispatch data object, you can get the size of all the buffers it represents with the dispatch data get size API. And we also provide you with a singleton object that represents the empty buffer. This is very useful when you do incremental concatenation as the starting point of the concatenation.

Or if you ask us for a sub-range of an object where the sub-range doesn't actually exist, we will return you this object. Now, if you actually need access to the buffers directly that underlie a dispatch data object, we have a couple of ways to give you that. One is the dispatch data create map API. This is the one case where we make a copy with the dispatch data API. This will create a single contiguous piece of memory from all the possibly discontinuous ones that are represented. So in this example, again, here if you call dispatch data create map, we will copy the data and make a single piece that we represent with this new map object, and we will fill in the buffer and size variables that you've passed in by reference with the start of that new buffer and its size. So when the source object goes away, of course, this copy will stick around, and this is why we need this map object. Once you release that map object, this is when we know that we can get rid of the copy, and that's also when that buffer parameter will become invalid, potentially a dangling pointer. it.

Of course, if the piece, if the data object that you give us already represents a single contiguous memory area, we don't make a copy when you call the map API. So here we will just make a second object that points to the same buffer and give you a direct pointer into it. So hopefully this explains why you should not modify the memory that's behind those buffers that you get from this API, because you could actually affect somebody else's memory that somebody else in a different object is also seeing. If you want to avoid making these copies, we also provide you with a way to get the single piece of an object that represents a single piece of the discontiguous memory buffers with the dispatch data copy region API. So again, the same example. Here, we're actually looking just for the piece that's at the location where that light blue arrow is.

We call this API. It gives us a dispatch data object that represents just that one single contiguous piece at that location. So this allows you to do any custom traversal that you may need to do. And because you know, we guarantee that the region object that you get here has a contiguous piece, you can just map this without making any copies. If you want to just traverse serially across one of these dispatch data objects, we also provide you with the dispatch data apply API, which will just apply an iterator block to each of these contiguous buffers in turn. So this example with the four buffers, we will just apply the block to each one of those pieces.

So let's look at an example of this in action. Here we acquire a dispatch data object from somewhere, and what we're interested in is to try and find the header on a character by character basis. We want to look through the data and find the terminator character of a header here, which we picked control Z as an example to be the terminator. And so we need to fill in this position under under block variable with the position of that character in the buffer. So we call dispatch data apply and our iterator block will get passed four parameters, a region object, which we don't actually use in this example, but that just is an object that represents the current memory area that's being traversed, and an offset which indicates where that area starts in the overall data object, and a buffer and a size that gives you direct access to the storage. So here we just look for the position of that terminator character. If we find it, we calculate the overall position in the buffer, and we return a Boolean that indicates whether the iteration should continue. So for instance, here, once we have found the header, there's no need to go through all the rest of the buffers in the object, so we can return false and stop the iteration.

Once we have found that position, we can create a sub-range object that just carves out that header piece and get rid of, release the rest of the data object, which may release some of the buffers if they're no longer needed. So an area where these dispatch data objects are particularly useful is a new API set that we're introducing in Lion and iOS 5 called DispatchIO. This provides you with a facility for doing asynchronous IO from file descriptors and paths.

And the goal is to extend existing GCD patterns in your code to areas of your code where you have POSIX-level file and network I.O. And by allowing you to express your patterns of asynchronous I.O. more clearly to us, we can optimize process-wide across multiple subsystems any I.O. that may be ongoing. So the sort of things that we optimize are that we handle non-blocking network I.O.

for you. We will set any sockets that you pass to us to be non-blocking, watch them for network activity, and read data from the memory drive, the IO loop, all that behind the scenes for you. If you have multiple physical devices, multiple hard drives, for instance, in a Mac, any IOs that can go to different devices, we will perform concurrently. But if you are doing I/O to a single device, we will optimize it correctly, not submit too much work to the device to overwhelm it and pipeline it so that you get optimal performance. As an example of that, here we did an experiment on an iPad 2 with Dispatch I/O reading 640 files of varying file size, and as you can see, the gray line is just reading each of those files in turn with the read sys call, reading the whole file, And the blue line is putting in an asynchronous IO request for all these files into the system and letting Dispatch.io pipeline those read requests for you. And as you can see, by us doing that pipelining, we can get much improved throughput. So other advantages for you to use Dispatch.io are that you can avoid having any of your threads blocked in IOSIS calls. We will take care of doing that on a background thread for you and use the optimal number of threads for your device configuration. And it also allows you to manage any IO buffers with these dispatch data objects that we just went over. And importantly, dispatch IO allows you to do incremental processing of partial data. So if you have to go through a very large file, for instance, and can process on an ongoing basis, you don't need to read the whole file into memory, and it's very simple to use these patterns with the dispatch IO API. So the central concept in dispatch IO are channels. these encapsulate IO policy on file descriptors or paths.

and also allow us to track file descriptor ownership with a reference-counted object. And these dispatch IO channels have two different types, stream or random, that you have to specify a channel creation time with the following two constants. The stream access channels perform IO operations at the file pointer position and advance the file pointer position.

And if there's multiple asynchronous IO operations in flight, these will be performed serially, one after the other, because they depend on the file pointer position. Of course, if you give us a file descriptor like a socket where reads can be performed concurrently with writes without interfering with each other, we will do that. The random access channels, on the other hand, will start at a specified offset. The I operations on those channels will start at a specified offset to the initial file pointer position when you created the channel. And these operations then change the file pointer position. So this is like p read versus read, right? And in this case, any asynchronous I operations that you submit may be performed concurrently if it makes sense for the device in question.

And, of course, any file descriptors that you give us for these kinds of channels must be seekable. So three ways to create dispatch IO channels. The dispatch IO create API, where you pass in a file descriptor. The dispatch IO create with pass API, where you pass in a path on disk. And the dispatch IO create with IO API, where you pass in an existing IO channel. This is useful if you need to change the type, for instance, of a channel. Or change configuration parameters on a channel without affecting the original. And all of these take a cleanup block as the last parameter. So let's look at that. This cleanup block is what allows us to do that wrapping and control of file descriptors.

When you give us a file descriptor with the dispatch IOCreate API, it becomes under system control, and you must not change it during that time. So you must not do any direct I.O. to it or change the file pointer position or close it or change any configuration parameters on it. And you must not do these things until that cleanup handler is called. That cleanup handler gets called when the I.O. operations that you have submitted to the channel have all completed, and either the channel has been released or it has been closed with an API that we'll see in a couple of slides. The syntax of these cleanup handles is very simple. We just pass in an error. These errors are only creation errors for the channel. So we only pass you errors that occur when we try to create a channel, for instance, if you pass a bad file descriptor or one of the wrong type or a pass to a volume that doesn't exist or anything like that. You won't get any I.O. errors on the channel that occur later on when you submit operations. And the typical thing that you will do in this cleanup handler is close the file descriptor. Again, you must not do that before this point because it would rip it out from under us. The I.O. operations that we support are asynchronous reads and asynchronous writes. You submit an asynchronous read request at the file pointer or at the specified object offsets, depending on the channel type, with the dispatch IO read API. And here you just give us the lengths that you would like to read from that channel. And asynchronous write, you pass in, again, occurs at the file pointer position or at the specified offset, and you just pass in the dispatch data object that you would like to write to the channel. Both of these take a handler block, which is an I/O handler. This is what allows you to do incremental processing. We will call this block multiple times, potentially, during the asynchronous I/O operation and give you incremental pieces of data as they come in. So importantly, that data that comes in, we will get rid of it as soon as the handler returns. So if you can do any ongoing processing, that's great. Otherwise, you must retain that data if you need it after the handler has returned. So just call dispatch retain on it or concatenate it with another object, a dispatch data object, et cetera.

And the read operations are passed the data in one of these handlers that was read since the last time the handler was called, whereas the write operations actually pass the data that we couldn't write yet. So this is the end piece of the data, which will get smaller as the write proceeds. And this is particularly useful if the write fails. Then you can get data that we couldn't write and maybe write it somewhere else or retry or something like that. The syntax of these handlers is we pass you three parameters, a Boolean, done flag, a dispatch data object, and an error parameter. And so you should treat all of these three parameters independently and check for their presence. If there is a data object, that means maybe there was some data read, you process it and handle any error that occurred. And once you get the done flag, that indicates to you that this is the last time it will call this handler, so complete any processing of incremental data or any state that you've accumulated during that time. The other kind of operation we support on dispatch IO channels are barriers. These are very similar to the barrier blocks that we discussed earlier for concurrent queues. They only execute once any pending IO operations that you already submitted to the channel have completed. So this is a way to allow you to synchronize IO operations on the channel and know when they have finished.

and while one of these barrier blocks is ongoing, it has exclusive access to the file descriptor underlying the channel. So we do actually allow non-destructive modifications of the file descriptor during this time. So here we submit one of these barrier blocks with the dispatch IO barrier API, and inside this block, now we are sure when we're executing that we have exclusive access to the file descriptor. So if you don't know it, for instance, if you open the channel from a path, we will open the file descriptor on your behalf. So you don't have it.

You call the dispatch IO get descriptor API to get it. And in this example, we just call fsync, for instance. So if you have a bunch of asynchronous writes that you have submitted, you want to make sure that they're synced, you put in a barrier block that will make sure they have all completed and now sync. I mentioned closing channels before. You do that with the dispatch IO close API. This will close the channel from accepting any new operations, but it will let any existing already submitted operations complete. Once they have all completed, it will invoke that cleanup handler where you can close the file descriptor. If you need to interrupt any already submitted existing operations, you can pass in the dispatch IO stop flag, and we will attempt to interrupt any ongoing IO at the earliest possible opportunity, but of course, this is asynchronous. So again, you must wait for the cleanup handler to be called to be sure that all the operations have actually completed. Thank you. And any ongoing I/O that's-- any I/O handlers that are interrupted get past the eCancel error flag set that they know that they were interrupted by a stop.

So to go through an example of all this tied together, we are implementing a transliteration of maybe a very, very large file that we pass in via a file descriptor, and we want to transliterate that file on a character-by-character basis and write it out to a path and want to do it all asynchronously. So we create an input channel. In this example, we make it a random channel from this file descriptor, and as you can see, the cleanup handler here just handles any creation errors and then closes the file descriptor behind us when the IO operations are done on this input channel. The output channel we create from a path, and importantly for this example, we make that a stream type, and we put in the parameters that we would otherwise pass to open to create the file for writing. And the cleanup handler here just handles any errors. It doesn't need to close any file descriptors because we do that on your behalf when we open from the path. Once you have that all set up, you can put in an asynchronous read operation to the input channel, and here we pass in offset zero and length size max, which will actually read the whole file on disk. So it could be 200 megabyte file or something like that. And we have the handler block here, which gets passed those done data and error parameters. And remember, this gets called potentially multiple times. So each time we check, do we have a data object? If there is one, we call this transliterate function that I haven't shown here, which will go through and create a new data object with the result of this transformation that we want to do. And create a new data object that returns to us that we now pass to an asynchronous write.

So this is where it becomes important that the output channel was a stream channel because, of course, we could have multiple of these asynchronous writes in flight if there's multiple pieces that have arrived via the multiple invocations of the read handler. So it must be a stream channel so that we don't jumble up the output pieces. And we just pass the output data piece at offset zero to be written to the channel. And we, in this instance, in the IO handler, we don't do anything special. We just handle any errors. We don't try to write any data if it couldn't retry writing any data or anything like that. Once we have passed the output data object to dispatch area right, the system will retain that object so we can get rid of it here immediately. And the same with the input data object. We didn't, as remember, as I mentioned, this data object gets released immediately as soon as the read handler returns. So here, if you have a very large file, we will get pieces of that file one after the other and we will actually never have all the file in memory. As soon as any processing and writing of that piece is done, the memory will be released and you will have very good memory efficiency with this API.

When you passed the done flag in the read handler, you know that the read request has completed so we know that we never need the output channel anymore. So now we can just release it. And the same with the input channel. Once we have submitted the read request, the system will retain the input channel for the lifetime of that request so we can immediately get rid of that channel. So now at the end here we have released all our references to all the objects. And as soon as the asynchronous IO completes, all the references, all the objects will take care of themselves and release. To summarize, we went over a couple of advanced examples of usage of the target queue API The new APIs that we are introducing in iOS 5 and Lion, concurrent queues and barrier blocks, which are useful in particular for implementing read-or-write schemes, queue-specific data, which allows you to associate arbitrary key-value storage to a dispatch queue, dispatch data API for managing discontinuous memory buffers, and the dispatch IO API for asynchronous IO. For more information, you can contact Michael Churowicz, our DevTools and Performance Evangelist, And we have a concurrency programming guide on the DevTools website, which is very informative and covers many of these APIs. And of course, libdispatch itself is open source. We will very soon merge in the source changes for the new APIs that we are introducing into that repository. So if you want to go and have a look at the implementation, you can do that. And of course, there's a special forum for GCD questions on the developer forum. So we encourage you to ask any questions today. Related sessions happened all yesterday already, unfortunately, so if you haven't had a chance to go to those, please watch them on video. And I've also put the references to the GCD sessions from last year on here. That's focused specifically on iOS programming with GCD. Thank you.