The iPhone app we're working on has become quite large and complex - necessarily so. However, from the start of development many months ago the product has grown organically. Although almost feature complete - the app was quite unstable and failed to deliver in terms of performance so some refactoring was needed.
As I worked my way through spaghetti code, I came across many threading objects and asynchronous calls that were initiated and 'managed' all over the application, sometimes in the view, sometimes the model, and sometimes on the controllers. This effectively made it impossible to determine what the application was doing at any given time. To make matters worse it it was not clear that we were being thread-safe as access to some shared data was unsynchronized. These findings were the likely root of our stability issues. In addition to this we found some tight thread-sleep loops. These were a possible candidate for some performance issues but also a total abomination.
To tackle these issues I had a plan. I would create a framework that provided the threading functionality required by our application use-cases in a safe and centralized manner. I'll now describe the framework.
The foundation of the framework is an old favourite - the command pattern. Any piece of work we want to do on a thread - be it make an HTTP call, parse a document, or asynchronously write to the database - is expressed as a command. In practice we must implement a protocol:
@protocol Command
- (void) execute;
@end
Concrete command classes usually pass some input parameters in the constructor and provide an accessor so that the caller can extract any result of the processing that takes place in the execute method. The execute method implementation is the 'stuff' you actually want to DO be it synchronously or asyncrhonously.
Fundamentally the framework executes commands. However, regardless of your concrete command implementation - the framework can execute your command in a synchronous (blocking) or asynchronous manner. What a relief for the developers - forget about how we safely do something asynchronously and concentrate on the thing you actually want to DO. The framework provides the following methods on the core class - CommandSchedulerQueue - for command execution:
- (void) addCommandToQueueAndReturn:(id<Command>)command;
- (void) addCommandToQueueAndReturn:(id<Command>)command
- (void) addCommandToQueueAndWait:(id<Command>)command
As the method names suggest - the framework provides a command queue internally which mandates that the commands be executed in the sequence that they were added to the queue. This may sound a little restrictive but it is perfectly possible to create multiple command queues or even allow more than one command to be executed concurrently on a given queue by allocating the queue more threads.
So before I describe the functionality given to commands and callbacks by the framework, let me mention what's going on inside the framework - and specifically the thing we call the CommandSchedulerQueue class. From the outset I wanted to stick to using well understood Foundation components and classes. Indeed the Foundation framework provides a rich set of classes to deal with concurrency - so use them! At the core of the CommandSchedulerQueue class is an NSOperationQueue, we wrap our command execution in an NSOperation and schedule it on the operation queue. This is nothing new - our application used NSOpeationQueues in the past but often created them all over the place and applied them in an inconsistent manner. By wrapping an NSOperationQueue in our CommandSchedulerQueue we can add useful behavior and hide NSOperationQueue specifics that previously tempted developers to do undesirable things.
So what kind of behavior do we add to the command execution?
- We can block the caller until the operation - or rather the command - is complete. We do this in a safe and performant manner using the NSCondition class.
- We can invoke a callback to the caller if they have scheduled a command to be executed asyncronously.
- We can capture an exception raised during the command's execution and either throw it back to the caller immediately if the command was executed synchronously, or we can deliver the exception in the callback in the case of asynchronous execution.
Let's take a look at the callback:
@protocol Callback
- (void) callbackInvokedForCommand:(id<Command>)command ;
- (void) throwExecutionException;
@end
@protocol MutableCallback <Callback>
- (void) setExecutionException:(NSException*)exception;
@end
When implementing the callback for a command we need to provide the behavior of the callbackInvoked method - i.e what we want to happen when the callback is finally fired. Often we'll want to send a message to the original caller to tell them that their command is no longer executing. The receiver of the callback can then invoke throwExecutionException to handle any exception that occurred during the execution of the command. An implementation of throwExecutionException is actually provided in an abstract <MutableCallback> implementation and the CommandSchedulerQueue knows how to set the exception within the callback object.
So that's the basic framework. In our iPhone application we now put all concurrent functionality through this framework which allows us to keep the threading complexity in one place and provides us with excellent visibility on what the app is doing at any given time. We can schedule commands knowing that they'll be executed in sequence and with the confidence that we won't miss any exceptions thrown - even if they're thrown on another thread. Finally we can schedule operations and block until completion in a clean manner (no tight thread-sleep loops!) while hiding the specifics of NSCondition.
How does NSInvocationOperation fit in with your framework? I have been using this class to execute "commands" asynchronously. As you suggested, it is unwise to have developers mucking with NSOperationQueues directly, so I wonder if I should be doing something like your CommandSchedulerQueue. So far it has been fairly straight forward to use NSInvocationOperation.
ReplyDelete