Mac OS X Essentials • 1:03:52
Multithreading and multiprocessing are powerful techniques you can use to improve your Cocoa application. Find out why and how to separate an application into independent pieces, and ensure those pieces interact smoothly. This is a good opportunity to build on your knowledge of multithreading and interprocess communication.
Speaker: Chris Kane
Unlisted on Apple Developer site
Transcript
This transcript has potential transcription errors. We are working on an improved version.
And welcome to Session 139, Partitioning your Cocoa Application. My name is Chris Kane, and I'm a software engineer with the Cocoa team. So what I'm going to be talking about today - I'm going to define that term partitioning that you saw in the title. I'm going to give several examples of when and why you would be using this, what really is a design technique.
I'm going to talk about a few of the pitfalls, I'm going to look at several you know, sort of aspects of partitioning in a little bit more detail, and I'm going to finally at the end give you some concrete code examples. So if you find yourself you know, just really want to see code examples and zoning out during the first half of the talk, hang in there. We'll get to some code examples eventually.
I'm going to have to assume in this talk that you have some knowledge of multi-threading or, and inter-process communication, and I'm going to use terms like thread, and I'm going to have to assume you know what that is. Or I'm going to use a term like socket and assume you know what that is.
I'm not going to define these things, or go into more detail about how you do socket based communication, I'm going to assume you can, after the talk if you don't know what they are, can go and investigate more about these things on your own. Finally we're just going to talk about some multi-process and multi-threading reasons for partitioning today. Although there are good reasons for using partitioning, or other reasons as well.
So what do I mean by partitioning? Well, first and foremost partitioning is a design technique. One might call it factoring. We almost did, but we decided against that because we didn't want everybody to think oh this was going to be a talk about re-factoring. No, what we mean by partitioning is the segregation of parts of an application into well defined and independent pieces. And then those pieces have to in some way communicate with one another.
So what we're talking about is a design technique where basically you're trying to either build walls because you want to keep things independent, or you have walls in place for some reason, like you have multiple processes and you have to deal with those walls in some way. So to summarize right away, there are two main aspects to partitioning, the segregation into independent parts, and then the communication between the parts.
So to illustrate what I mean in more concrete terms, I'm going to go through a series of examples of, reasons for and examples of partitioning. So the first is the Unix command line tool. We want to make use, well the first is we want to make use of other applications or executables, applications or executables which somebody else has written, somebody else is providing, and the Unix command line tool is my first example. If you want to invoke a Unix command line tool from within your application, you're going to be dealing with the kind of issues that you're, arise in partitioning.
So when you, after you have created the command line tool, you know, started it in some way, you have to communicate with it. And there are several different ways you might communicate with the command line tool, such as passing arguments to it when you start it, you might use a pipe or a socket, or Unix signals. And the file system can also be a way to communicate with the command line tool. So this is the kind of thing I'm talking about. You have a separate process, in this case a command line tool, and you have to communicate with it in some way.
Another example is the services menu in your application. When you have a services menu, you have things like mail the selection to somebody else. If you have a selection of text in your code, and you, the user chooses the mail to service, the mail application gets that data, and puts up a compose window.
So that's an example again, of a form of partitioning, where the services menu is making use of services provided by other applications. In that particular case, most of the communication is going on via the pace board. So the selected text for example, is put on the pace board, and the mail app reads it off.
But mach ports and the file system can also be involved in the services communication. So those are other examples of the communication going on in that case. When you have a scriptable application, you are invoking services, or capabilities I should say, often that other scripted, scriptable application, and you are effectively communicating with the scripting engine, that is the scripting libraries, and whatever mechanism it uses to do the actual communication back and forth.
You know, Apple events at the high level. Another reason you might use other applications and other executables is to use services on the local network or on the Internet. So if you're say talking to some API that is vended by Amazon dot com, this is another type of partitioning that's going on, where you are making use of something going on on another machine, in this case off at Amazon dot com. In this case you're probably communicating with sockets, and possibly a higher level protocol, like SOAP or HTTP. If you're communicating between multiple Mac OS X machines you can use of course, distributed objects. If you're you know, communicating with something that you've written on the other end.
Another reason to use partitioning is to make use of multiple platform architectures. So there are several different kinds of platform choices that we now have available in the system. For example, there is Intel or PowerPC, the microprocessor choice. There is 64-bit process or 32-bit process. There is garbage collected and non-garbage collected. And of course there are different operating systems. You know, Mac OS X and, or Windows I list here, of course or Linux, probably should have added that in the slide.
And these are properties of the entire process, so that is if you want to do something in 64-bit, the entire process has to be 64-bit. You cannot have you know, most of it be 32-bit, and you got a little bit of it to be 64-bit. The entire process has to be 64-bit. So if you need to do something that involves being, using 32-bit code and using 64-bit code, you have to have multiple processes involved.
Well why would you want to do that? Well if you have for example, existing plug-ins that are 32-bit and, or perhaps they're Power PC, and you want to continue using them in some way, then you have to create, but you want your app for, in this example say, to you know, be converted to 64-bit, you have to continue, well you have to create a new process, a 32-bit process to load those plug-ins, and then communicate with that 32-bit process 2in order to make use of that, those old plug-ins in some way.
Another example might be suppose you're using an open source library, and it just isn't ready for being compiled as 64-bit. d You know, perhaps it assumes that ints and pointers are the same size, and they're interchangeable. So when you go and try to you know, compile it as 64-bit, and you try to run, you know, it just crashes all over the place, or something like that.
And you know, you just don't have the resources to port it yourself, you may have to use partitioning. That is create a 32-bit proces2s which links with that open source library, and then define some sort of protocol or communication channel with that thirty two bit process in order to continue using the capabilities of that presumably important open source library that you're using.
So what do you do? Well, as I said before, you're going to have to create a separate helper process, which you know, you're going to launch when you first need it, and then you're going to communicate with it via some sort of communication channel, inter-process communication channel, like pipes or sockets, or the file system again, can be a form of communication channel by writing a file that that other thing picks up off the file system.
Distributed objects here is mentioned, and I mention this in particular at this point because we have made in Leopard distributed objects 64-bit capable,d because we you know, had to make the whole thing 64-bit. But distributed objects can also talk to 32-bit processes from 64-bit processeds, and vice versa.
And in fact we will do some forms of argument type coercion and what not to you know, be able to continue to talk between the 64-bit and 32-bit processes where method type signatures may change slightly. Because you know, one thing it may now be using a long, but in a 32-bit space it's using an int, and that kind of thing. And so as long as the long value fits into the 32-bit integer,2 we'll you know, convert back and forth.
Another reason to use partitioning is to make use of another machine. So you may want to use another machine's operating system, there may be something that an operating system can do for you. Or it may have software, other software on that operating system that you need to make use of. You may want to simply use its harbor resources. For example, if you use distributed builds and Xcode, you're simply making use of the other machine's you know, CPU, its disk space, its memory to do your compilation.
Another reason is you may have special hardware which is you know, somewhere off on a network, hooked up to that particular machine, and you want, if you want to access that special hardware, it might be you know, a simple case would be a printer. Then you would be using a form of partitioning in order to talk to that printer. Now you generally wouldn't talk to a printer yourself, you would go through a library to you know, do the printing to the other machine. So that's other subsystem, that printing subsystem is doing the inter-process communication and the partitioning for you.
So you have to create and use a server process in that other machine in order to be able to you know, make use of that other machine's resources, its hardware, whatever it happens to be. And again, example of communication include pipes and sockets, distributed objects if you have a Cocoa server running on the other side, or you know, some sort of custom protocol.
It might be SOAP or HTTP based, or it might just be a custom protocol of you know, a custom byte stream over the sockets that you're using. Or you might use a library which is provided for you from you know, somewhere else, like a third party might provide a library that talks to its server, which you run on the other machine.
A fourth reason for partitioning is to increase safety. The first example might be just data security, second might be privilege level. So if you want to run something at a higher privilege, or a lower privilege, or simply as a different user, you may want to create a separate process in order to do that.
A second process can also give you a separate protected address space in which to you know, run code in, a separate address space which is different from your main applications address space. And so that can give you some additional safety, and I'll get to some examples in a second.
A separate address space, a separate process, is another way to achieve multi threading, that is each of those processes will have at least one threat. But because it's often a separate address space, you can in some sense avoid many of the multi threading issues that may arise if you're trying to do actual multiple threads within your own application directly.
So what are some examples? Well, I think the most compelling example might be again, the plug in. Suppose you have a desire to have a plug in model, or you have a plug in model for your application, but you don't want to load those plug-ins right into your application. That is maybe there's information or things that you don't want the plug-ins to be able to access, you know, if they were to be loaded right into your application.
Well, creating a separate process, you can load the plug-ins into the separate process, and have them off in their own little world so they can't get access to your data. And if they happen to be malfunctioning, or malicious in some way, they you know, cannot take down your application, they'll take down the helper process. In fact, this is what Spotlight does.
If you've ever done a process listing while Spotlight is indexing your machine, you would have seen these MD worker processes running. And the MD workers are separate processes which are loading the Spotlight plug-ins, and running them, but they're separate from the actual Spotlight daemon, which you know, sort of controls the Spotlight database.
Let's see. And again, well as I said before, you have to create the helper process for this. A fifth and final reason that I'm going to cover is separation or encapsulation of functionality. And this is a little abstract, a little vague, so let me give some examples. Well one reason for this is simply to improve the structure of your application.
If you have independent parts you might have a better structured application that gives you more you know, more flexibility in making changes or what not. So this, that's still a little vague. You may want to separate the computation, and the data involved in some computation, from its presentation in the UI.
One example might be in the you know, the new Leopard system. We have introduced NS operation that I'll talk about in a little bit. And so that's an example of you taking a computation and the data it needs, and trading a separate entity for it so that it can be run on another thread in the background usually.
You may want to create separate data domains. For example, you may want to say okay, I'm going to create a thread, and I'm going to make that thread responsible for accessing say this core data file. And doing all, that thread is going to do all of the core data stuff.
And so I don't have to worry then about multiple threads trying to access that core data file, that core data store. I'm going to make that thread responsible. But once I've done that then, I need to define some way to communicate with that thread, tell that thread to do things with that core data store.
So I've you know, you can avoid some of the thread safety issues, and some of the threading issues by doing that kind of thing. But you need to then define the communication channel, because you've built that wall between you know, the rest of your app and that particular subsystem.
So again, you know, another reason to use partitioning is to make use of multiple cores, but also potentially to help you increase the thread safety at the same time. That is if you make things, if you can make things independent, then you don't have the particular thread safety issues that generally arise. The primary reason for thread safety issues is of course, you know, if you were at my talk last year, or have you know, read books on the subject, changes to shared data on multiple threads.
So if you avoid changes to shared data on multiple threads, you can avoid a lot of the thread, threading issues that arise. I talked so far about a lot of examples of communicating between multiple processes. Let me give a few examples of communicating when you have multiple threads within the same process. Well the first, and very simplest communication channel is of course memory.
You can store something in memory, and another thread can come along and read memory, and look that up. The threads share memory. More formalized abstractions include queues for example. If you have a producer and a consumer thread, they may communicate via a queue to maintain their independence. The queue then becomes the communication channel between them.
Arguments or initial data, and return values or result data can also be a form of communication. That is if you create a new thread and you pass it you know, a selector to run, and the argument to run with, you're passing it its initial data, and that can be a way to, that is, not can be, it is a way to communicate with that other thread.
The inter-process communication mechanisms I already mentioned usually work here too, so you can use sockets between threads if you want, you can use distributed objects, you can you know, use the file system as a communication channel. But those are generally not going to be as efficient. We used to recommend distributed objects between threads, but our recommendation now is not to use distributed objects between threads, because it's generally heavier weight mechanism than the other alternatives. That is if distributed objects between threads works for you that's fine, but you know, distributed objects has its own setup, and you know, behavioral costs that you have to deal with.
So what I would recommend instead is using the new perform selector on thread mechanism that we've added in Leopard, and that's the bullet right above the big red X. So I have a big red X there, and I don't want to say don't you know, absolutely don't use distributed objects between threads, it's no longer a recommendation.
You're better off if you can choosing something lighter weight, you'll get better speedup out of your multiple core multi threading process. What are some of the pitfalls in partitioning? I warned you I would talk about this at the beginning of the talk. Well the first is performance can be a pitfall The extra layering and communication that's involved in communicating between these independent parts can slow things down.
That's interesting. It's, the text is clipped for me, it's not for you. If you're you know, having to serialize some objects into some sort of byte representation, send it across to another process which unpacks that, and then you know, you know, recreates all the objects, obviously that extra layering involves overhead. You're not simply messaging the objects, you're going through this extra transportation layer.
Responsiveness though might increase with the use of additional cores. So if you're doing multi threading, and you create you know, operations, or you create threads, or whatever mechanism you happen to choose, the overall efficiency I'll say might decrease. That is it costs you more to be multi threaded in any context.
However, getting to the end goal by using multiple cores is faster, generally. So that is the reason for using the multiple cores to say split up work or do something, things in parallel. You get to the end result faster. But overall, if you summed up you know, all of the cost, the overall efficiency is less.
A second pitfall in partitioning is what I'm calling versionitis. When you have a communication channel, the senders and the receivers have to agree on the communication protocol. If you write some bytes out to a socket, and the other side reads those bytes, the other side has to know how to interpret those bytes.
And so if you change things in the sender, in your application, and recompile, and rerun, but you don't rerun, recompile and change you know, and recompile your server and re-launch it, then you know, to match of course, then of course you have a communication problem. Because the other side is not going to interpret those bytes correctly, or it's not going to know how to interpret those bytes that you've sent. So that's an example of versionitis. Changes have to be coordinated on both sides, which adds to the complexity of things.
A third, and the final example I'm going to give, of pitfalls and partitioning is what I call execution context dependencies. And I think this is my favorite, because it's, can be very, very insidious. Code may expect to run within a certain process or in a certain thread. That is it is dependent on its context. When you write the code, you may unintentionally make assumptions that for example, the code may assume it can access the NS application object.
Well if you take that code, and you put it off in a separate process, and it tries to access the application object, well you know, maybe you got a link failure or some compiler warnings that you dealt with, and realized it at that point. But if it does manage to talk to some application object in that other process, it's obviously not the same application object that it was used to talking to when the code was in your application.
So that's an example of context dependence. Another example would be per thread data. If some code runs, and it puts something into per thread data, and then you know, other things run, and later the code runs again, that function or that method runs again, and it expects to find that same per thread data that it put in there before in the per thread data data store, it may not if the thread, if that function is now running on a different thread.
Let me give you an example. We had an engineer at Apple who wanted to load the plug-ins that his app was running, or loading, at launch, all from a separate thread. And the hope was well I'll increase launch time if I you know, put these off in a background thread, then my main thread you know, can do the other launch activities, and get some performance out of that.
Well he did that. And what he found was well those, nothing happened then with those plug-ins, because those plug-ins were accessing the current thread's run loop, and installing their own run loop sources. Well so when he put the loading of the plug-ins off on a background thread, they were installing their sources on the background thread's run loop, which of course the background thread went away after the loading was done, you know, it didn't stick around. And so the plug-ins had installed their sources, but that got torn down when the thread got torn down, and the plug-ins were not able to get any input.
So he had to go back and put the plug-ins back in the main thread, the loading of the plug-ins. So this you know, illustrates how context dependence can be really invisible, really insidious, until you've actually gone and done the work, and then find out maybe oh, I can't do that. So if you remember back at the start of the talk, we had this slide.
And I talked about there being two aspects to partitioning, segregation and communication between the parts. Let's talk a little bit about segregation. So multi, multiple processes is one way to do segregation, or to get segregation. The only way to handle multiple platform architectures is to use multiple processes. Those binary choices I talked about, 32-bit versus 64-bit, garbage collected versus non-garbage collected, are all process wide, properties, and so you have to use multiple processes if you want to use multiple of those.
Let's see. I already talked about you know, multiple processes giving you more address space to work with. You know, suppose your app cannot be 32-bit at this point. You may be able to create a helper process to do you know, the heavy work where you need 64-bit, and so you can communicate with the 64-bit process to do that specific work that needs to be 64-bit. But your app itself doesn't have to be converted yet to be 64-bit capable.
However, when you have multiple processes, getting the data back and forth, communicating between them is more labor intensive. It requires more code for you to write, but it is also more costly in terms of CPU usage. And you get less intimate access you know, there's of course less sharing. And that's part of the point of partitioning, that there's less sharing between the multiple pieces.
When you need the other process, you have to go and start it. We recommend of course, as we do every year, that you use the highest level API that suits your needs. NS task and NS workspace can both be used, their Cocoa APIs to start other applications. Launch services is what a lot of NS workspace is based on. If you need to drop down and you know, get some of the extra capabilities and launch services, you can do that.
In Leopard the core OS guys have added posick (assumed spelling) spawn, which is a call to create a process directly without going through the fork and exec pair. But of course as you know, in other Unix systems you can still call fork and exec. And of course there's launch D, too, which is often appropriate if you have a server situation, to launch that server when you first need it. And then you have to communicate using one of the techniques I talked about before. Maybe you've provided arguments, or maybe you used pipes and sockets, or something else.
Multiple threads is a second form of part, segregation, partitioning that I'm going to talk about in this talk. It's often simpler to start up and shut down threads than it is processes, so that's one benefit of using threads. And it's easier and faster to communicate back and forth, because of course they're sharing a memory address space, and so you can just use memory.
But using multiple threads requires more discipline in order to maintain the segregation. If you're going to use multiple threads within Cocoa, we say just use NS thread. NS thread has been improved in Leopard to be much more sub-classable, if you actually want to sub-class NS thread, and we've given it some more methods as well.
To communicate between the threads you have to you know, use one of the communication channels I've talked about. The best way is to give the thread, or other objects that will be doing the work on that thread, all of the data up front. But you can also do inter-thread communication if you need to do some communication later. Or if objects are thread-safe, each of the threads of course can talk directly to those thread-safe objects.
NS operation is a new API in Leopard that is one form of partitioning, or encapsulation, computation encapsulation maybe I should say, that we have invented to help you do multi threading, what would be the word? Programming.
- Multi threading for dummies?
- Well no, it's not multi threading for dummies, but it's a, the point of a NS operation is to provide an abstraction around which you can you know, think about your multiple, use of multiple threads.
When you're going to use an NS operation, what you do is you generally create an NS operation sub-class. And in the simple case you simply have to override its main method, the method called main. We provide an NS operation, sorry, NS invocation operation sub-class of NS operation, which you know, has the capability of taking a target object and a selector to invoke, and, or an NS invocation to invoke on the other thread, if you have some existing method that you simply want to invoke from, in the context of an operation.
The benefit of using an NS operation is that you can put them in an operation queue, and the operation queue will create threads for you, and it will start jobs, and run you know, a few jobs at a time, hopefully. The intent is that we're running enough jobs to keep the CPUs busy.
But you can also start NS operations manually. You know, it's just an API. You could create one for example, and pass it off to some other sub-system in your application, and that other sub-system when it was ready for that operation to run could you know, start it manually at that time, or put it in a queue.
The key thing is that NS operations must be KVO compliant. And as long as they're KVO compliant, they can be put into an NS operation queue. Simple operations, those that just override main, have all the KVO compliance taken care of for them by the NS operation abstract super class.
Once you have an operation that's running, or have an operation, again the best thing to do is to give it all its parameters up front. For example, if you had an operation object which was you know, the intent was to say warp an image in some way, the best thing to do is to give it the image when you create the NS operation.
You might give the NS operation sub-class an init with image initializer for example, give it all the data up front, then it can go off later and you know, play with that data. But of course you can do interthread communication, or message thread-safe objects directly. And I'm going to point you to the documentation, which is fairly complete in your Leopard seed to learn more about NS operation, and NS operation queue.
The second aspect of partitioning was the communication channel. The purpose of the communication channel is to act as a mediary between the independent parts in order to help maintain the separation independence of those parts. Usually communication is done via one way messages, and usually it's between simply two endpoints. If you want to have any sort of reply or return value, that's usually implemented with a second one way message that goes back to the original sender, or original thread, original process, whatever it happens to be.
The sender, once it sends a one way message, usually immediately continues with you know, doing other work. But if the sender needs to get a reply back from the other side, the sender can immediately choose to block, waiting for a response message. If you use DO, this is exactly what DO is doing. It sends a one way message off, and then immediately chooses to block, waiting for the reply when you have a two way message.
Beta containment handoff is an, a particular topic within communication channel, the communication channel subject that I want to call out here as you know, something worthy of your attention as you think about partitioning your application. The point of data containment and handoff is to keep objects from being referenced outside of the independent parts.
That is if the parts are independent, it's nice to hide as much as you can within those independent parts to keep other parts from unintentionally getting around your communication channel in some way, and talking to them directly. So the point is to hide references if you can within the separate communication channels.
The best way to do this is when you send information to the other side, you send it and then you forget about it. It's sort of like when you have a relay race, and one runner hands off the baton to the next runner. At one point one runner has it, and at another point the other runner has it.
The goal is to not access objects or data from multiple threads. When you have sharing, you have access from multiple threads to the same data, you have the potential for the usual multi threading issues that arise. When references to objects do get exposed across a communication channel, that's called reference leakage. So there are many forms of reference leakage, and some of them are very subtle.
When you put objects in global memory, that can be a form of reference leakage, because another thread could come along and read that reference out of memory. An example of this is the NS notification center. If you add an object as an observer to the NS notification center, that has put it into global memory.
And if any thread posts notifications that that observer has registered for, those other threads will be messaging that object. And you may not expect that object to be being messaged from multiple threads, but if you've put it in, any observer within the notification center may receive messages on multiple threads.
If you give the object that you want to keep hidden to a non-contained object, you know, an object which is not part of that independent piece, then you may have leaked it. You know, an example would just be like a set method. If you set it in another object, you may have leaked it. Because other objects could get the reference from that non-contained object.
One example of this is suppose you're going to send an array to the, to another thread. And so what you do is you say well I'm going to copy the array and give the copy to the other thread. But the copy may not have copied all the objects inside of the array, and so all of those objects, references now, have been leaked to that other thread. That other thread will access the array, and all of the objects inside of it are now, you know, the references to them are shared between those two threads.
And finally, another form of reference leakage is to simply pass an argument and a message to some sub-system. You may not know what that other method is going to do to that object reference, and so that may be a vector for leaking references. And so that's you know, this is a much deeper topic really than I can go into, but I'm trying to give you sort of a sense of some of the complexity that can arise here.
When you have multi threading and a communication channel between multiple threads, the channel should be thread-safe. The independent parts potentially do not have to be thread-safe. The point of the segregation is that you know, you're not sharing data or anything across them, but the channel itself does need to be thread-safe usually. Channels used between processes may not have to be thread-safe unless you have multiple threads using the same channel to get to another process.
Okay, finally, finally we're going to get to some data, some examples. My particular example here, I'm going to focus on using the new perform selector on thread mechanism that has been added in Leopard to Cocoa. So this method takes four arguments, the selector to invoke the thread on which to invoke it, an argument object for the method, and a wait until done parameter which specifies whether you want the perform method to block until the method has been performed in the other thread, or return immediately.
And this can be used to send one way messages back and forth. So you, what you do is you, when you have a method you want an object to perform, you send that object, the target object the perform method, and that will become then the receiver in that other, the context of that other thread.
Now perform selector of course is very useful, and many of you may be thinking oh finally they've added this. You know, they've had perform on main thread for many releases now, but I haven't been able to target my methods to a specific thread, what if it's not the main thread.
But there are a couple potential issues with this. The first is that you have to know that you need to use perform selector on thread. That is you have to know you can't just message the object directly with obscene messaging as you normally would. You have to know that that object only wants to receive messages on that other thread. So that's the first issue. The second issue is that second argument up there. The thread. You have to know what thread to send the message on.
The third issue is the argument. There's only one of them. So if you have multiple arguments that you need to get to that other method, what you end up having to do is creating a package of the arguments, and create a cover method which can unpack the arguments, and then send a real message in the context of that other thread.
And the fourth issue of course, is the versionitis that can happen. The senders all need to know how to package up the arguments in a way that matches what the receiver object is going to do in unpacking them. One solution is what I'm going to call a bridge object, and you could also call this a proxy object. It may sound like a proxy object, but I'm going to use the term bridge in this case.
The point of the bridge will be to hide all of that knowledge that I just talked about, all that knowledge that was needed. It's going to hide the actual destination thread, and potentially the destination object, it's going to hide the packing and unpacking work that needs to go on if there are multiple arguments. So this is just you know, object oriented encapsulation. I've had a, I have a bunch of information, a bunch of policy, whatever you want to call it, and so I've created a class to encapsulate all of that knowledge and work that needs to go on.
This bridge object then is going to be this, all the senders' endpoint in the communication channel, to get to that other object in that other thread. They're going to talk to the bridge object, rather than to talk to that other object directly. And of course this applies equally to inter-process communication, where if you're communicating bytes over a socket, you talk, it'd be very convenient to encapsulate all the serialization work, and the unserialization work on the other side into a bridge object that you know, knows about those things. So every send point does not have to replicate that information. In my particular case I'm talking between two threads, and I'm going to use perform on thread to talk between them.
As a twist, I am going to now eliminate this bridge object that I just talked about, in one sense I'm going to eliminate it. That is I have an object which only wants to process its messages in the context of another thread. It sort of wants to only be used on that other thread. But it has a public API. So what the object is going to do is it's going to bridge to itself, and so it's going to act as its own bridge.
What's going to happen is the object is going to receive messages from various senders in the source side on the left. The object is going to tell itself to perform a slightly different internal private message on its thread, it knows about the thread of course that it wants to run on. The perform queue is our communication channel in this case, and it eventually is going to deliver the message, and the object is going to do the work on that other thread.
So that's you know, fairly straight forward, should be fairly straight forward. So I have a method here in the API of this bridge object called do work, and it returns one way void to indicate that it's asynchronous. It's going to call perform selector on itself, and tell itself to do this private internal method called ox do work in this case.
It's going to send that, it's going to ask that that message be sent to itself on the my thread, which is presumably some sort of instance variable of the object, and it's going to say wait until done no. So this is going to return immediately. Eventually the other thread will, the perform queue on that other thread will call the ox do work on that object, and the object will do its work.
If you have multiple arguments, the changes in gold here are what change in that process. I've given the do work method an integer argument, and what's going to happen is that I'm going to use an NS dictionary to package up the arguments. So that's fairly straight forward, you know. The dictionary will have the string object, and the integer argument has values for some well defined keys that I'll define.
I'm going to pass that argument dictionary as the argument to the perform selector on thread method. And the perform selector on thread method by the way, retains the target object, and it retains the argument object until the method has been performed by that other thread. So my dictionary, which has auto released in this case let's say, is safe, it's held by the perform queue until that method has been invoked.
Eventually the method is invoked on the other thread, and it gets the dictionary as its argument this time. First it has to unpack the arguments, but then it proceeds and does its you know, the work that it did before in our previous example. So that has solved most of our sending issues. What about return values? Suppose I want to effect, have the effect of return values or reply messages, but communicate via perform selector on thread.
Another issue that I'll point out at this point is that the sender may need the result of a computation, and may need to wait in some way for that reply message, that reply information to come back. And so we'd be looking at both of those things. Well the first solution is to use two perform selector on thread invocations, two one way messages as I indicated before.
In the first case you, you know, start, and the first object, object A, and you send it a message. And then the object might you know, continue on doing work, or it might have to block, waiting for the reply immediately. And eventually the other object sends a message back in some way using perform selector on thread.
Well, this has some of the same sending issues that I started with. Now, it's the receiver object, that remote object in that other thread, yeah, sits on the right, which has to send messages back to the original object. And so it has to know what thread the original object wants those messages on, it has to know what the selector is to send, and basically it has to bridge back to the original sender. So using two one way messages, you have to do bridging then in the both directions. The sender may eventually have to wait for the return perform to occur on its thread, and so it needs to wait in some way.
A second solution is a little bit better. In this case we're going to have the bridge object do the waiting, and get the return value back to the original thread. So what I'm going to do is I'm going to say this do work method say returns an NS string. And so I've changed the return value to NS string. And this method, although it's going to do the work in the other thread, is going to return the reply information, the return value, the result string, whatever you want to call it as its return value.
So what I'm going to do is I'm going to allocate some memory on the stack, the local stack of this thread in which the result is going to be stored, I'm going to create an NS value with the pointer to that storage location, and I'm going to package that return, that NS value up with the arguments in the NS dictionary that I'm going to send over to the other side, just as in my previous example. This time I'm going to say wait until done yes. So the perform selector method here is going to block until the method has been performed in the other thread, and the method invocation is finished in the other thread.
So eventually the other thread will execute the method, and it has to unpack the arguments and the result address, and do the work. As its last act, it's going to take that address, it's going to extract the address out of the value, and store the result string in there. It's going to give the result string also, and retain.
So its given the retain to keep that NS string result object alive until the other thread can get at it. So what I've done basically here is, the other thread, the background thread has written into the stack at the storage location I told it to, the result string object.
So eventually the perform selector method then, which has blocked, will return because the method in the other thread returns, and the result on the stack has been filled in by that other thread. So what I'm going to do is I'm simply going to return it with an auto release so that it becomes you know, auto released in the context of this thread, just like any other API, you know, it normally returns an NS string.
So that solution works pretty well, and pretty neatly has solved the problem of getting the information back to theg thread. It's all been encapsulated quite nicely within the bridge object itself. But we've lost something. We've lost the potential for parallel work. In the first example, the method was sent off to the background thread, and then the original sender could continue on and do work. And that kind of parallelism might get you a performance increase.
But we've lost that here because when you call the do work method, the sender then has to block, waiting for that return result, and the blocking of course is done by the wait until done yes argument. So a third solution will allow us to restore that asynchronicity, and that is to use what I'm going to call a future value. A future value is simply an object which holds another value. It's like an NS value but it's mutable.
What the future value's going to do is if you try to get the value out of the future value, it's going to block you if the value hasn't been set yet. If the value has been set, it's going to return the value that's been set right away. But if you try to get the value that hasn't been set, it will block.
Now this is not a new concept, this concept goes back thirty years or more. It was called eventual values in the literature long ago, and it's very similar to a concept in parallel, or concurrent functional programming called a future. Here's what the interface to this future value might look like. Of course there's a getter and a setter, and there's an IVAR which will store our value. And I'm going to use an NS condition lock to implement the synchronization that's needed.
So in the bridge, what the bridge is going to do is it's going to return the future, a future value instead of the NS string. And the future value is, acts like a token for the actual eventual result of the computation. So the do work method is going to create a future value to hold the result, and pass that as the arguments in the dictionary to the other thread. We're going to go back to wait until done no. So we're going to get back the return immediately from the perform selector on thread method.
And we're going to return to future value right away. Eventually on the other thread, this internal private method in the bridge object will be invoked, and it will unpack its arguments and do the work as before. And instead of writing to the other thread stack, it's going to simply call set value on the future value to store its result. So the result is now being stored in the future value rather than on the stack.
What does this look like from the point of view from the caller? Well the caller has of course, a handle on the bridging object, and it calls the do work method there. It gets back the future value, and it's going to store it, it's going to retain it and store it somewhere for later use. Because in my example here, the caller does not need the result right away. So it's going to go off, having stored the value, and do some other stuff.
Eventually, presumably, it needs the return value, why else you know, you have the other thread do the work? So it needs the return value. So it asks the stored future value for its value. If the value has been set, that is the other thread finished its computation, this method will return right away with the NS string result.
If this value has not been set in the future value, the value method, the getter will block until it is set. Eventually, presumably, the value is set, this method will return, and the result has been retrieved. And at some later point we remember to you know, release the stored future value as well.
What does the future value implementation look like? That's the most interesting part here. Well this implementation that I get here is not complete in and of itself. It's obviously missing a dialic (assumed spelling) method, and there are a number of things you can do to improve this, you know, to do better than this. But this is the amount of code that happens to fit on a slide, so that's what I included here.
When we initialize the future value, all we need to do is set up the condition lock. So we do that by creating a condition lock, and initializing it with condition zero. What this condition lock is going to do is act as a simple Boolean latch. That is it's going to be no, and eventually the value's going to be set, and then the latch is going to have the true value. So zero is going to represent no the value has not been set, and one will represent yes the value has been set.
In set value, all the future does is lock the condition unconditionally, store the value that its given, and unlock the condition with value one, which means yes, now I have the value. So the condition lock in this case is acting also as a Boolean to say you know, yes or no, I have the value been set. When this unlock condition happens, all the waiters who are waiting for condition one will wake up.
What the getters does is lock when condition one. That is the getter doesn't want to proceed unless the value has been set. The getter doesn't want to return something that isn't there yet. So the getter potentially blocks here in lock with condition until the value is set. If the condition's already at one, this immediately returns. If it isn't at one yet, the condition lock won't allow the lock, and will block this thread until it has been set.
We unlock the lock to balance the lock. And we simply unlock it, we don't change the condition, because of course the getter is not changing whether the future value has or has not you know, gotten the object, it's you know, value yet. And we return the value IVAR. So fairly straight forward.
Now that's a very simple sort of example, but it's a very powerful concept. One way to extend it might be that a future value could instead hold many values, there could be many results. You don't have to necessarily package them all up into a single value. And of course there are other ways to you know, make use of this. The future value could be itself passed around as a token for the eventual return value. But until somebody actually needs that return value, nobody needs to block.
So it sort of acts as a stand in for the eventual value that will be computed by that other thread. And this has helped restore the potential for parallelism in the API. That is if the caller really does need the return value right away, it can call the getter that it you know, on the future value that it got from the do work method, and it will block there, and you know, wait for the return value to happen. But if it doesn't need it right away, it can go off and do other things waiting for the result to be filled in.
This kind of blocking though can still be problematic for the UI, because it's not a type of blocking that is running the run loop or allowing events to be processed. So you know, that's one way again, in which potentially that future value that I presented here could be improved. Well let's see. I think it was Steve Jobs who introduced the word wow to us on Monday, so I'll use that again, wow. Let's wrap up.
Partitioning is an important design tool, and there are two main aspects to partitioning, the segregation into independent parts, and the communication between the parts. And there are times when you're going to have to use partitioning because you simply have to do something that can only be done by say using multiple threads or multiple processes.
But it can also be used as a you know, voluntarily as a way to help structure your code, or increase your thread safety. We looked at several reasons for partitioning, and gave several examples. And we looked at you know, some issues, in one example of a communication channel which is using the new perform selector on thread mechanism.
But as always, this talk has simply scratched the surface of the potentially very deep subject. And so the next time you have a problem which could potentially be solved using partitioning, I encourage you to go out and look at some of these things that I've mentioned in this talk in more depth.
Thank you.
( applause )
Now you've seen this before, but it always bears repeating. Deric Horn is the Application Technologies evangelist, so he's the evangelist perhaps mostly responsible for, most responsible for Cocoa. And of course the usual documentation link. And the documentation in your WWDC seed as I mentioned before, is fairly complete with respect to NS operation. And there's also a nice multi threading document that the documentation people have written in there.