Mac • 49:02
Grand Central Dispatch (GCD) lets you write concurrent and asynchronous code without the burden of direct thread management. Learn from the experts how to simplify your multithreaded code base by transitioning to GCD. A must-attend session for anyone interested in GCD with experience in other threading models.
Speakers: Kevin Van Vechten, Dave Zarzycki
Unlisted on Apple Developer site
Downloads from Apple
Transcript
This transcript has potential transcription errors. We are working on an improved version.
Good afternoon, my name is Kevin Van Vechten, I'm the manager of the GCD team, and later I'll be joined by Dave Zarzycki who's the technical lead for GCD. And today we're going to talk about migrating your applications to Grand Central Dispatch. So to start out, we'll do a quick technology overview, kind of very briefly recap some of the things that we covered in this morning's session and yesterday in case you missed it. But we're not going to go into very much of that, so hopefully you'll have a chance to catch those on video if you haven't seen them.
And then we're going to do a discussion of what it takes to take a single-threaded application which many of you may have, and adopt GCD, potentially get some concurrency, some asynchronous behavior leading to a more responsive user interface at all times. And then the rest of the talk will be devoted to converting multi-threaded applications, whether that's Cocoa or POSIX layer, you're probably using a lot of pthread in POSIX API, and so we'll deal with those individually and talk about how they map into the GCD API.
So for our technology overview, I just want to reiterate that GCD is part of libSystem and it's available to all applications on Mac OS X. It doesn't matter if you're targeting the POSIX layer or your Carbon application or a Cocoa application, everything can take advantage of GCD. As part of libSystem, there's no special link instructions that you need to do, all you need to do is include the header file, and that is dispatch/dispatch.h. So here is our technology, the API footprint isn't very large, we categorize it into a few high level areas.
GCD has simply polymorphic objects that respond to retain and release as our memory model. And the primary interface with GCD is that of queues, and queues run, queues of blocks, it's an asynchronous scheduling engine. You can take a block of code, you can submit it to a queue, and the system will run it for you asynchronously. And so we've covered all these APIs in our earlier sessions.
Again, please check out the video if you haven't seen it. So queues are lightweight list of blocks for asynchronous execution and a very important characteristic of the queues is that all of the Enqueue and Dequeue is FIFO. Some queues execute blocks concurrently, they still Dequeue in FIFO order, but they don't wait for the previous block to finish before starting the next one, and you can get concurrency on a single queue.
Other queues, the ones that your applications create, are serial and FIFO. So they're going to wait for a previous block to finish before starting the next block. However, two queues running at the same time might be concurrent with respect to each other even though individually they're running blocks serially. And here's a little animation just to recap.
You might have a thread, it might create a block, submit that block to a dispatch queue, the system will bring an automatic thread online, begin processing the block, and when the block finishes, everything will go back to a steady state. And it takes a very little amount of code to do that, that whole diagram and animation you just saw is the instantiation of this code. Dispatch async, a target queue to the where the block should be submitted, and then the block of code to run whatever you want to put in that block.
So we can take this and very easily bridge single-threaded applications into a potentially asynchronous or concurrent world. And the key to this is to identify independent subtasks in your application. A lot of times when we think of concurrency, we think of taking some array operation and really trying to break up the math into small chunks or a lot of parallel computation.
But actually GCD is more about task level concurrency. And it turns out that in a lot of applications, especially UI applications, there are a lot of different operations that might be going on at the same time. Potentially every menu item that the user selects might kick off some sort of asynchronous operation.
And yes, at times those different operations are going to need to coordinate with each other when they're saving changes back into the document or something like that. But there are a lot of independent tasks, and each one of those independent tasks is an opportunity for concurrency in a lot of cases.
And even if you're not going to get that much concurrency depending on what your document structure is like, at least if you use dispatch to get those operations off of the main thread, you'll be able to avoid the spinning beach ball cursor, you'll be able to maintain a very responsive user inner space because the main thread is free to process new events from the user, and all of the real heavy lifting is being done in the background by an asynchronous block.
And the syntax of blocks is really powerful, you can take your existing code and with very little modification, you can wrap that code in a block, and it still has access to all your variables, you don't really have to refactor into a complete callback design in the traditional C sense. And we've provided an API dispatch group async which let's you fire off multiple blocks to any number of queues that you want, and then wait for them to all finish.
And if you're coming from a single-threaded application, this is really convenient because a lot of times a single-threaded application is going to make the assumption that it has the results from all the previous operations ready to proceed in the next step. And so using a dispatch group and then waiting on the group allows you to effectively fan out your serial application, your single-threaded application to multiple threads for a brief period of time, get a little bit of a performance boost, and then when those concurrent operations finish, you can collect all the results and continue with the serial single-threaded approach.
So here's an example of this in practice. We might have three relatively independent tasks. If you got a chance to see the state of the union talk on Monday with the Seeker [phonetic] application, they actually took an approach fairly similar to this, they had a lot objects they were modeling in the solar system and rendering, and it was very easy to just group them into a few different groups, use the dispatch group API to treat each of those groups independently, and then wait for the results to complete.
And you saw what a dramatic speed boost they were able to get from a relatively small amount of code change. So we can take this code here where we're executing three tasks; foo, bar, baz, and using the results of all three of the tasks. And we can look at them and say okay, well serially it's going to take a certain amount of time, but if we have two cores available, we might be able to take less time and execute some of these concurrently.
And if we had four cores available, we could take even less time. And that can be done with a relatively small amount of code. So this code sample is using the dispatch global concurrent queue, we're getting a reference to that at the top, and we're also creating a group.
And we're defining our result variables with the under under block keyword. And if you've seen the talks on blocks, you know that allows a block to modify the contents of a variable. So each of these blocks that we run can just assign their result back into this local variable in the current stack frame. And then we use dispatch group async. Notice I'm only using it for the first two operations, bar and baz.
Foo we're going to keep on the current thread, there's really no point spawning off three threads only to keep one of them idle waiting for the other three to finish. So we can actually use the current thread for part of the operation and farm off the rest of the work.
And in fact, if you happen to know with certainty that one of the operations is consistently going to take longer than the other operations, it usually makes sense to keep the longer operation on the current thread and then the other threads can come on and complete the shorter operations. And you know those results will be ready when the primary thread is finished.
If there's no real certainty that one operation is longer than the others, then it really doesn't matter too much, you can just pick one. But here we've executed bar and baz asynchronously, we used the current thread to execute foo, and then we do a group wait to wait for the results. And then we release the group because we're done with it at this point, and we're free to use the three parts of our computation.
And so what we've done is we've taken the elapsed time for this operation, and instead of being the sum of the three parts, it's now just the length of the longest of the three parts, which can be a pretty dramatic boost, especially if you have a lot of operations that you're doing. And so for a single-threaded application, really with no additional functions defined by your application, no additional data structure is defined by your application, no additional allocations by your application, all of this is managed by the GCD API and the use of blocks.
You can just wrap some of your API with the use of dispatch groups and you get an optimal amount of concurrency. So from a single-threaded application to a multi-threaded application, you don't need to worry about how many processors are available, just break it up into logical subtasks and as many processors are available to the work, we'll start working on it. But it'll scale gracefully from a two-core system to a 16 virtual core system.
And dispatch is very low overhead, we think you'll be very pleased with the performance of these API and with the intelligence of the resource management. And this means that there are more CPU cycles executing your code and less time spent in the system figuring out how to do concurrency.
And they're just a lot of opportunities for easy speed gains or again, even if the speed gain is relatively neutral because you might have serial activity just getting that code off of the main thread can be a big benefit to your users by maintaining that responsive user interface. So now let's talk a little bit about multi-threaded applications. There are a lot of patterns that map to GCD.
And you will see as we go into this more detail, that there are a lot of existing patterns that are very different from each other that happened to map into the same GCD patterns over and over. What we want to point out is that it's easy to adopt GCD on a case by case basis.
You don't need to go rewrite your whole application, you can really pick and choose little pieces of your application and evaluate GCD in that specific area. And we encourage you to really focus on the return on investment for converting to GCD both in terms of the productivity to you as a programmer because a lot of times it's very syntactically concise to use GCD.
It works about the same but it's a lot less code, and that might be a great thing by itself. But actually GCD is very computationally efficient, and so we think in a lot of cases you'll also find out not only is it more concise and less code, but it's going to perform quite a bit better too, especially when going from single-threaded to multi-threaded. With the existing multi-threaded app it might be more of a lateral transition.
So to start off with multi-threading, we wanted to do a little high level summary of GCD and Cocoa and how they work well together, and some of the existing Cocoa patterns that you might be used to, and how that translates to GCD. So again, with Cocoa and all the event processing and UI drawing happening on the main thread, running code in the background by itself is a big win even without additional concurrency. But you probably need to run a lot of code on the main thread too precisely to do these UI updates and to synchronize your view updates.
And dispatch makes it really easy to take code off the main thread, put it on a background thread, get the result from that, bring it back to the main thread. Not only is it possible to use GCD with Cocoa, it's actually got some conveniences. For example, all GCD queues implicitly provide an NSAutoRelease pool, so you don't get those messages that you're running on a thread and you've created an auto release object and you don't have a pool available.
I'm sure you've seen those in the syslog. We take care of that for you. however, because the threads are frequently reused, there's no real strict guarantee about how frequently the auto release pool is drained. So as always, if you're going to be doing a lot of allocations, it does make sense to manage your own auto release pools within a queue, but it's not going to leak objects, there is a pool provided for you by default. And GCD also coordinates with the garbage collector, it's perfectly fine to use GCD in any garbage collected applications, but we'd like to point out that the dispatch objects themselves strictly follow the retain/release model.
You must use dispatch/retain and dispatch/release on those, they are not collectible. So they interoperate with garbage collection quite well, but not automatically collectible. So the dispatch main queue is a special queue that's also fairly important when you're dealing with Cocoa applications because it's the GCD interface to getting code running on the main run loop.
And it cooperates with the main run loop, all the code will run on the main thread, it's going to run serialized as you would expect from the main run loop. And it's going to be running NEGCD blocks in the common modes of the main run loop. Now because all serial queues are FIFO and non re-entrant, that also means that the main queue is non re-entrant. And what that means is if you're running the main run loop and then you recursively rerun the main run loop in a sub mode, that won't continue to drain blocks from GCD if you're already in a GCD block.
In other words, if you're in a block and recursively rerunning the CFRunLoop or the NSRunLoop, we're not going to pool anymore GCD blocks off because the current GCD block hasn't finished yet. So it really only goes that one layer deep, it's not recursive. So here's several patterns that you're probably pretty familiar with, with multi-threaded programming in Cocoa, a whole variety of perform selectors.
performSelector:onThread:withObject:waitUntilDone, performSelectorOnMainThread, performSelectorInBackground, or just generally performSelector:afterDelay. And GCD has great equivalence for each of these. In the first case, when you're talking performing a selector on an arbitrary thread, well our model is not to use threads specifically, it's to use queues and to allow the system to automatically create threads on your behalf. But it looks pretty similar, you can use dispatch async to perform a block on a queue, and that block can call whatever selector you wish. But what's nice about it is it can call any number of selectors, and those selectors can take any number of arguments.
So in the past where you likely had to define your own selector to work in combination with perform selector, and you probably had to take multiple arguments and put them all into an NS dictionary, and then in your selector grab that NS dictionary, extract all the keys, a lot of boiler plate type stuff, now you can do that all in the block because the block captures all of the local variables, you can call any sequence of selectors you want. And dispatch asyn means enqueue the block and continue moving, so that's the equivalent of wait until done with a no value. And we also provide an equivalent of wait until done with a yes value, and that is to call dispatch sync.
Very similar API, just one letter different. When you want to target the main thread specifically, very familiar pattern, it's the same dispatch async or dispatch sync depending on whether you want to wait for the result, but all you need to do is specify the dispatch main queue instead of your own queue, and that's highlighted in this slide by DispatchGetMainQueue.
That returns a reference to the main queue. When you want to perform a selector in the background, really in GCD that's the same as submitting a block to one of the global queues. And the global queues will execute blocks concurrently, they don't wait for completion, you're just kind of adding another block to the mix to be run in the background.
And that's exactly what this sample code does. And again, it just gives you all the flexibility of blocks and none of the inconvenience of kind of conforming to the perform selector protocol. And finally, we have performSelector:afterDelay. And as we talked about this morning, we have the concept of time in GCD which allows you to specify arbitrary timeouts. And in this case we're specifying a timeout that is 50 microseconds from now. And we can use a dispatch after API, it'll perform a block after that period of time has lapsed, it'll get submitted to the queue at that point.
So very similar to performSelector:afterDelay. So to recap, we think this is a very convenient approach because it allows you to call any selector you want or call multiple selectors with no need to pack arguments, no need to unpack arguments. And queues provide a very simple and consistent model for concurrency. There's no need to think about how to send data to another thread, how to get that thread to notice that you've sent data to it, that's all taken care of for you by the active submitting a block to a queue.
And so now to talk about in more detail migrating to GCD from POSIX applications, I'd like to welcome Dave on stage.
[ Applause ]
Thanks, Kevin. I hope you all are beginning after our sessions to see how easy blocks and queues can be. So let's talk about how blocks, queues, and POSIX get along. And let's talk about the GCD advantages. So GCD, as we've illustrated throughout the week and talked about a lot, is a lot more efficient. We're trying to make a lot more CPU cycles available to your code to use all those cores across the system.
We're also trying to make it easier to get your code running on those cores. So we're also providing better metaphors, blocks are easier to use, you don't need to worry about marshalling all of this data into some kind of context data structure in the heap, and then freeing it and unpacking it from the struct. Blocks allow automatic capture, you can delegate to the libraries of the block copy and block release, and it's just a lot easier to use. We'd also like to point out that a very classic computer science metaphor of producer and consumer is a queue.
And that's what GCD is, it's all about queues and blocks. And lastly, but I think really importantly, as much as you could take a textbook, learn about threads, learn about thread pools, go to the effort of applying all of the recipes you read in the book about how to create a thread pool. There's still one step the book doesn't describe. There's a lot of different teams, a lot of different companies all trying to work together on an average computer a customer has. And there's no way that everybody can coordinate and share one thread pool without an OS solution.
And we're providing that to provide a nice holistic shared high performance system. So on the topic of compatibility, we'd like to point out that even if you adopt GCD, you don't need to run out and drop your existing code. Mutexes, condition variables, threads, they get along just fine in the process along with GCD.
So you could even send a block out, block a mutex, use a condition variable, all just works. We'd also like to point out that GCD threads are actually wrapping POSIX threads, it is a thread pool. And what that means is don't cancel, exit, kill, join, detach a GCD thread. You didn't create it, don't destroy. It's pretty simple. GCD also reuses threads, and this is how we get a nice boost in performance.
And what that means for you as a programmer is that you need to not mutate thread state without saving and restoring the previous state. So if you want to for example, change the current work and directory of a thread, you probably ought to change it back or else all the subsequent reads to the thread will be terribly confused. In practice this isn't much of a problem for any code, but something to keep in mind.
And finally the result of pthread_self can change between block execution but not during the execution of a block. So in order of complexity, what I'm going to talk about in terms of migrating from POSIX to GCD, is once functions, threads, forking and joining threads, mutexes, condition variables, and then is a subset of condition variables.
We'll talk about producing consumer threads and what that looks like. We'll talk about thread pools briefly. And then finally we'll talk about how many of you have probably dealt with external events, perhaps with a thread that's waiting on select or kqueue or something like that. So let's jump in.
Once functions. It's a real simple topic, it ensures that multiple threads do two things; they can execute a function on the fly but only once, and then any other thread that zooms in will see one of three states, it's either going to see that the function hasn't run and needs to run, it is running, or it's complete.
So one of the threads will win the race, start running the function. The other threads, if they happen to trip across the same path, will wait for it to complete, and then after that point every thread will see that the function is completed and no longer needs to be run. So why do you do this? It's all about lazy initialization of global variables.
It might be because the resource is expensive or otherwise can't be allocated at compile time. In pthreads, you're going to have some kind of global, in this case, my context, you'll have a pthread once variable, that should be declared static, I apologize. There's an init routine you'll have to declare somewhere in your file, it will do the initialization, it's this case it's a malloc and some kind of library init routine. And then somewhere later in your code, perhaps in different places, there is a call to pthread once, can that takes the address of our predicate, the pthread once variable, and the routine which was probably declared far away in the file somewhere.
And then you can do something with that data after we know that it's been lazily initialized. So what I'd like to point out here, and it's not so obvious in a simple slide, is that these different variables and context could probably be spread far apart in your file, and it makes it harder to see at a glance what's going on.
However, with dispatch, we can do something similar, we can declare our struct and our predicate a dispatch once t. But in one simple spot we can put all the code in initialization logic that's probably nearest its actual usage. So we could take dispatch once, take the address of the once variable, and pass it a block. And then we know after dispatch once returns we can do something with the initialized data. So that's dispatch once. It's really convenient and it's efficient, it's actually more efficient than pthread once.
Threads. Well what do pthreads do? They execute a function concurrently. And why do people do that? Well they want to avoid blocking the current thread, they don't want that spinning beach ball, or they want to achieve some computational concurrency. In pthreads, probably the simplest form of a pthread creation is pthreadCreate, you need to get the pthread handle back, and you need to pass in the null as an optional parameter, so we'll pass null.
Work func is defined somewhere else, implemented somewhere else, a context, then check to see if it failed. And then finally we have to take that handle and say no, no, no, never mind, we really just don't even care, we want to detach, and then we're done with that handle.
On GCD, it's a lot simpler and a lot faster. You get the global concurrent cue and you dispatch async. No return value to worry about, it will just be put into the queue and run later. And this is async F by the way, you can also use async if you wanted to submit a block.
But as a simple straightforward transform, this is the simplest. So that's a simple mapping of threads to dispatch async, it's really convenient, less things to worry about like errors or handles that need to be immediately cleaned up after. It's also insanely more efficient, you can get thread recycling, you can get better CPU scheduling because we can defer those blocks until a CPU becomes available.
Whereas pthreads more aggressively create threads and over commits. So a twist on threads is the fork/join model, and it's about doing background operations and keeping the local thread busy and then coming back together at some point. So what does that look like? It's a pthread create as before, we need to get that handle. Passing null, there's no interesting attributes for trying to set up on a thread. Passing a background function, named background in this case, and we'll pass into context. We'll check for failure and deal with that too.
We'll also run the foreground function with the same context, and we can now have two things running at the same time. After the foreground function completes we'll join, thus disposing of the pthread filing. Check that it didn't fail, and then finally we can do something with the results of the foreground and background function.
In GCD it's something similar, but actually in fact far more powerful. We can get the global concurrent queue, we're not creating, we're just getting it. We can create a group. We can then dispatch async into that dispatch group async into that group, and run our background method. We can then run our foreground method. Then we can wait on the group, and this case, wait forever, this is a dispatch time concept we talked about in both the first and second presentations this week. Then release the group, and finally do something with R1 and R2.
Now the unique thing here that gets really powerful is the group wait can wait on a lot of group asyncs, it can wait on them all. So rather than with the pthread model where you'd have to join, join, join, join, join for a lot of times, you can just wait once. And because of that, you could actually hand off the group to other subsystems which add other members to the group that the original creator doesn't know about. But the original waiter can still wait on them all.
So it's a lot more powerful. And we'd actually also like to add a new twist that pthreads can't even do, or at least not nearly as easily or conveniently. Instead of waiting on the set of blocks that are running within the group, your code can instead call DispatchGroupNotify.
DispatchGroupNotify will take the group, wait for it to empty out, and then run a block on a queue. And with that, you can then build very powerful designs that were much more complicated to express before. In these new designs you can fan out a bunch of work across different queues and then set up a little notification to know when it empties, reconcile the results, merge them together, and then fan back out again. So you're going from one set completion, everything is know and well stayed. Do a bunch of work, regroup, fan out, regroup, and it's a great way of maintaining your code.
So it's a really convenient alternative, especially with the block syntax. It's really efficient. It's also a lot more expressive as I was describing, you can join in multiple operations, you can also do the notification, and we believe you'll find that a lot easier to use. Mutexes. Real basic concept, but something GCD really win out on. Mutexes are from the perspective outside of a queue, you can think of it that way.
They're all about enforcing mutual exclusion to a critical section. From inside of the critical section, it's all about serialization, it's just a question of perspective. And the reason we all do that is to ensure data integrity. Computers at the end of the day are just reading a word of data and writing a word of data. So if you need to do something more complicated than just a read and a write, you need a mutex or a queue to protect the collection of reads and writes from memory.
So in pthreads, well everything might fail, so we need to check that acquiring the mutex didn't fail. We then can do our critical section, in this case we're trying to mutate a string. And if our reallocation fails, what we can do is kind of set our critical section to a known state and then unlock the mutex and return.
It's worth point out here that we could have forgotten to unlock the mutex and returned prematurely, and that is an interesting problem that mutex users need to be aware of. Finally, if the reallocation succeeded, we can concatenate our string, and finally unlock the mutex and verify that the unlock actually worked.
With GCD it's a lot simpler, you can just call DispatchSync. Only one thing at a time can happen on a serial queue, so this is a form of mutual exclusion. We can enter our critical section, do the same logic as we were doing before, and thankfully, thankfully, thankfully, thankfully because it's a block we can just return and dispatch sync will ensure that the critical data within the serial queue itself does not enter an inconsistent state. There's no way you can screw that part up. Thanks.
[ Laughter ]
Now something that isn't even possible with pthreads. What if you don't need to wait for the results of that critical section? What if there are no side effects your local thread cares about? Well guess what? Add one character, make it a dispatch async, and now that critical section can be run in the background, increased concurrency, and your local thread could move on and do better things. So this is a nice powerful addition that GCD provides that mutexes makes no affordances for.
So dispatch queues, serial queues, being used as an alternative to a mutex, they're really convenient, they're also very efficient, they're safe. You can not return without GCD reconciling the state of the queue and making it usable again. They're also more expressive, you can have deferrable critical sections as I showed. And GCD had wait-free synchronization which helps scale better. And because it's a queue, we're going to build on this concept and enqueue other things than basic blocks, and we'll do interesting things with them later.
So let's talk about condition variables. What are condition variables? This is the very POSIX pthread way of waiting for events. And in fact, they're not just waiting for any events, they're application defined events, and only application defined events. And the thread goes to sleep until the condition variable is signaled. It's common use, the most common use is about creating simple producer/consumer models, sometimes it's a set of consumers, but more often than not it's a set of producers and one consumer. The producer signals the consumer when work is added.
Now to show you what that code looks like - whoops. So unfortunately, POSIX requires you implementation your own model for events. It's a lot of code, and the most unfortunate part about it is that forces this problem. There's no convention between frameworks and applications for how to coordinate events, there's just real no established standard. And it means that there's no real interoperability between frameworks and applications and other frameworks for passing events easily between each other. However, dispatch queues are the solution.
It's a standardized metaphor, standardized API, and now it becomes really easy to have frameworks provide callbacks for any event. Or if you're working a large project in different teams, you can do this within your own application. So let's see what that looks like. I'll try not to bore you too much.
If you see this somewhere in your code, a thread being created with a function that looks like this, some kind of consumer thread, it's going to have some kind of infinite four loop, and then it's going to acquire a mutex that protects the data structures for tracking the amount of work the consumer and producer are agreeing upon. It's now going to wait forever around like a while loop, waiting for events to be enqueued.
And if there's no work, it waits on a condition variable, and then eventually the producer, which we'll see on the next slide, will signal that condition variable thus saying there's more work. We can then, that while loop will loop back around, we'll be able to succeed at finding an item. We'll get to this TAILQ_Remove.
We can now finally remove an item and proceed. We can unlock the mutex, thus allowing more work to be enqueued. And we can finally take that item and run it. So this right here is what one part of a producer/consumer will look like. Again, just look for a function with an infinite four loop and a thread condition wait, and that's what a consumer looks like. A producer, not quite inverse, but very similar, you have some kind of function, might take a context, might even take a function. All it's going to do is alloc some tracking structure to hand off to the consumer.
It's going to initialize that structure. It's going to lock the critical section that protects our TAILQ. It'll signal the condition variable if this is the first item being queued, insert the item, unlock the mutex so the consumer can use it. So these are two patterns you might see in your code. It's a lot of code, and we didn't even describe all the other things you would have to implement to make a producer and consumer safe. We didn't talk about any retain/release issues for these items or the data they track, that's more code you might have to implement.
We didn't show the data structure definitions for these items, and we didn't show the creation of that thread, or how you might do the appropriate tracking to know when you can clean it up. This is the GCD alternative.
[ Laughter ]
[ Applause ]
[ Laughter ]
So GCD provides a more convenient, more efficient model, and it's just plain a better use of your time and your intellectual energy. You've got more things to worry about. And it's fully integrated with OS frameworks, as you might have seen in other sessions. There are a lot of API now that are taking dispatch queues and/or blocks as parameters, and there's a lot greater events flying around the system now that are all queue-based.
Thread pools. Thread pools are really just a set of consumers in the POSIX pthread producer/consumer model. Now as I described earlier, it's really hard for any one subsystem to know what is the right number of consumers. And even with the best attempts of like asking what the current active number of CPUs are, that's only one subsystem, there are still other subsystems and there's no API for saying how many other thread pools there are out there.
There's no way of registering, it's just not a noble question. So GCD solves this problem with one consolidated thread pool. I'd also like to point out that this also shares the same problem of all the producer/consumer stuff that was just described, all the tedium, all the glue code, you have to do that with a thread pool.
And really it's just a dispatch global queue which you can get and don't even worry about retain/release issues because it's always there. So I'd like to move on to our last section where we're going to talk about external events. So there's a loose convention among POSIX programmers for how to deal with external events.
A framework might export a descriptor and a function. It is then the client's responsibility to get that descriptor and then monitor it somehow. The two most common ways of doing that are using select or kqueue. And then once that descriptor becomes readable, it's the application's responsibility to remember oh yeah, that descriptor is not mine, it's that framework.
And oh yeah, I need to call that function they gave us. And that's the very loose convention that is existent in Unix we believe today. We'd also like to point out that the POSIX standard essentially failed in this regard. POSIX condition variables can not monitor external events. It is the application's responsibility to somehow shim the events from the kernel into some kind of consumer with a condition variable.
Or alternatively, abandon condition variables altogether and use the kernel primitive to somehow wake a thread. Most common techniques are using a pipe or a socket pair to wake a thread instead of a condition variable. Again, instead of a condition variable you might see something like this on a thread, it's going to be an infinite four loop.
You're going to set up some bookkeeping to deal with just getting data into the kernel for what events you want to find out. In this case, for select we need to iterate some kind of event data structure, get the descriptors we're interested in, add them to an FD set, and then finally pass it into select, all the while dealing with signals that might come in and redriving the select.
Finally if that succeeds, we now need to evaluate the FD set that was modified by the kernel, iterate through our data structures, find the descriptors that fired, find our events, and then call the function that maps to that event and with the right context that maps to that descriptor.
So that's a lot of glue code, and it's all solved by GCD quite nicely. We have a standardized event tracking subsystem, you don't need to worry about draining any events, we'll just call your block when an event fires. This is great for both single-threaded and multi-threaded applications. We're not obligating you to go multi-threaded, just being event-based might make your life easier.
We also support both external and application defined events to really help you make your life easier. And there's no bridging you have to do, there's none of this getting a descriptor by the client, there's no finding out what function to call when that descriptor is readable. A framework can just install events on a queue and the application doesn't even need to know about it.
Finally, with dispatch there's no looping or scheduling that you have to do, the system does it all for you, we'll manage all the details. So for a single-threaded application, I'd like to give a quick example. Inside of main we can create a source to monitor standard in, find out when someone is pecking at the keyboard.
We'll direct the events to the main queue, we'll set a handler, and for brevity we'll use a function-based handler, and then we'll resume the source. That resumption let's GCD know that it can now start firing that function on that queue whenever there's an event on standard in. However, we're not done. We're using a library or maybe a set of libraries, or maybe even some internal libraries. And we need them to schedule events on queues.
So we can just pass in a queue and that library will say okay, great, I don't know what that queue is or where it is, but I'll register some events on that because you want this library to work. And finally we can call dispatch main, and now the program is ready to go. Events are registered, if they come in, they will execute on the main thread all serial just like you would expect a single-threaded application to work.
However, it's all event-based now, so a lot less code for you. A slightly different example with a multi-threaded application we're going to create a now background queue, and you know what, let's put all that standard in input on the background queue just because we're going to do some processing on that text before we redirect it to the main queue to draw the results.
So we're going to again create the source, set the function-based handler, resume the source so that way it can actually start running around and doing things. And then like before, we'll call the library initialization, tell it to use the main queue, and enter the main queues of end loop.
So in summary, this example is using two queues, the main queue and a background queue, and at no point did the application author need to write any kind of event draining logic or event, shimming logic, or any kind of redirection, just glue code, boring old glue code. It's all handled by GCD.
[ Applause ]
Yeah, we're quite happy about it too. So we have standardized event handling, there's consistent handling of all application defined events. You've got these queues, they're how everything reconciles with everything else; dispatch async, the resources, and all sorts of other events. We also support all the kqueue types for the modern BSD event delivery.
You can monitor Unix descriptors for readability, writability , or if you know it's a file descriptor that maps to a file system object, you can monitor from metadata updates on a file. So for example, if the file is deleted or renamed or revoked, the metadata itself is updated. You can monitor all those things.
We support timers, basic timers either on the abstract host clock or the more explicit wall clock for monitoring setting up an alarm so you can actually get up. Or you can monitor processes for reexecing themselves, forking, and exiting. You can also monitor a process for being signaled too.
And finally, again, no glue code. So that's migrating to GCD, your code is probably ready to adopt today, you could find little sections here and there of either producer/consumers, or mutexes, or signals, or event sources, they're just ready to adopt and strip away some code. It's convenient, it's efficient.
And we'd like to remind you it's a-la-carte. You can start small, find a little section here, give it a try, see how it works. We recommend you try and look for big wins and see what happens. Just verify that your goal has been accomplished. For more information, please contact Michael Jeruwitz, he's our grade developer tools and performance evangelist, he'll also be doing our Q&A. We have a developer forum for GCD so you can ask questions. We have documentation, both the awesome great concurrency guide that went online recently on the website, plus man pages and headed doc --