Popular Tags

Who's online

There are currently 0 users and 82 guests online.

User login

The Problem

When your applications become complex enough to start using multi-threading, it often becomes painfully obvious that allowing multiple threads to access and modify the same objects at the same time can be, well, painful. There's a whole new set of skills involving synchronization and locking that must be understood and acquired. This page only deals with synchronizing access to objects.

To understand the fundamental challenge we'll address, consider an object with several getters and setters, and assume that the object's behavior depends on the values it's storing internally. When you configure the object in a certain way, the outcome is always predictable, but if the settings aren't as you expect, the behavior won't be either.

To use a more tangible metaphor, imagine trying to use the same computer as your friend at the same time, each of you using your own keyboard and mouse. It gets pretty aggravating when you're trying to write a paper for school if he keeps switching to Unreal Tournament in the middle of your typing, and you resume your writing when he's just about to frag someone.

In a simple program, it's relatively easy to verify that everything works as expected. However, if another thread also modifies and uses the same object, it becomes impossible to predict whether the object will be in the state you expect at any moment in time. The standard technique for resolving this is to use a "lock", also often called a semaphore or mutex (short for "mutual exclusion lock"). For further background, read here:

http://en.wikipedia.org/wiki/Mutual_exclusion (even more detail at the "lock" link in the introduction)

Cocoa implements locking via classes that implement the NSLocking protocol, including NSLock and others. The documentation is concise and fairly clear — Googling for NSLock will also turn up several helpful resources. In general, it's safe to assume that Cocoa classes are not thread-safe unless they specify otherwise. Therefore, if you want to perform actions on something like an NSMutableArray without worrying about contention with other threads, you'll have to do it yourself.

For most programmers, dealing with threading issues is generally less fun than writing "normal" code, but sometime it's unavoidable. But before you jump straight into threaded coding, it's helpful to consider possible strategies for protecting shared resources and the implications behind them.

For existing classes, you can't just tack on an NSLock instance variable (although you could add methods via categories), so you have to create and manage your own lock across all threads. This can be somewhat complex, and I was curious, so I explored a few options for simplifying the process. Let's start with the more obvious, then proceed to the more abstract and esoteric. Why? Because it's fun!

The Separate Lock Approach

The most common and straightforward solution is to create an NSLock and require all threads to lock it (blocking until they can) before modifying the object, then unlock it afterward. Since every thread needs a reference to the lock, it must be passed together with the object that is to be protected, which tends to add parameters and/or methods to your API. Certainly workable, but perhaps something else is a more elegant solution for your specific situation?

The @synchronized Approach

Those that are quite familiar with Objective-C will recall that there is an @synchronized keyword that is used thusly:

- (void) myMethod:(id)anObject {
    ...
    @synchronized(anObject) {
    ... // critical section, do something with anObject
    }
    ...
}

As does a synchronized { ... } block in Java, this allows you to narrow the scope of a critical section to a specific block of statements. So clean, so elegant. Surely this must be the synchronization choice of the gods?

Just a moment though, @synchronized actually has a little secret. As it turns out, this construct is not compiled exactly as you might expect. It actually gets passed off to the runtime, which creates (i.e. allocates memory for) an NSLock on the fly and tracks it in relation to anObject. This doesn't mean it's necessarily a bad choice, just that you should be aware that it still uses the same primitive as our other approaches, as well as a little extra tracking work, so don't think you'll get off scott-free just by using the syntactic sugar. By all means, give it a shot and see if it works for you. It could be the best alternative, but it also might be less-than-optimal — your mileage may vary.

There are all sorts of nuances that go along with @synchronized, such as the object you choose to synchronize on. For further enrichment, read this post from bbum.

The Integrated Lock Approach

Familiar as we are with object-oriented programming techniques, it seems natural to want to have an NSLock instance variable inside the class we want to protect, and there are several ways to do that.

The One-Off Approach

If the object being shared among threads is an instance of a custom class, you can just add an NSLock instance variable and methods like -lock, -unlock, and -tryLock, then just pass those messages through to the NSLock. (Examples of this can be found in the code examples below.) One class, one instance variable, a few methods. Done and done.

The Superclass Approach

The previous approach is pretty simple, but one can imagine how a lot of code can be duplicated between custom classes. If it makes sense to share this behavior among multiple custom objects, you can implement this behavior in another custom class and introduce it as a superclass of all objects that need locking functionality.

This is easiest to do at the beginning of the design process but can be done later as well. One big objection to this approach is using a superclass that sort of "unites" its subclasses that otherwise have little or nothing in common. (Of course, one could say the same for NSObject...) Nevertheless, it is an option.

The Subclass Approach

If you don't control the source of the object you're using (for example, an NSMutableArray from the Cocoa framework) you can subclass it and add an ivar and methods just as before. However, changing every variable to use your custom subclass can be a pain in and of itself. In addition, extreme caution should be exercised when subclassing third-party classes, and especially Cocoa collections classes. Be aware of the existence of class clusters, and be forewarned that code that works for you might not always work as expected in different usage scenarios.

The "Dumb" Wrapper Approach

If we attack the problem from a different angle, it's possible to introduce integrated locks without modifying the class of the object being shared. Using composition, we can create a class that wraps the object of interest, together with a lock. Thus, we still pass around only one object instead of two, and don't risk modifying the behavior of the object in unpredictable ways.

The brain-dead approach is to create a simple container used only for storing two objects together in one. With this approach, any thread that wants to use the object (whether to read or modify) can get the lock, lock it, use the object, then unlock the lock, since both the lock and the object are freely available inside the wrapper. (This obviously breaks down somewhat if you have to pass the bare object into a method, but you can always lock before and unlock after the method call.) Below is a simple implementation:

#import <Foundation/Foundation.h>
 
@interface SimpleLockableObjectWrapper : NSObject
{
    id object;
    NSLock* lock;
}
 
- (id) initWithObject:(id)anObject;
- (id) object;
- (NSLock*) lock;
 
@end

#import "SimpleLockableObjectWrapper.h"
 
@implementation SimpleLockableObjectWrapper
 
- (void) dealloc {
	[object release];
	[lock release];
	[super dealloc];
}
 
- (id) init {
	return [self initWithObject:nil];
}
 
- (id) initWithObject:(id)anObject {
    if ([super init] == nil) return nil;
    // We'll ignore nil objects for now, and let the user cope with it.
    object = [anObject retain];
    lock = [[NSLock alloc] init];
    return self;
}
 
- (id) object {
    return object;
}
 
- (NSLock*) lock {
    return lock;
}
 
@end

This certainly works, but can we do better?

The "Somewhat Smart" Wrapper Approach

While pondering on the matter, I figured it might be cool if you didn't have to ask for the lock and object separately, and just called the methods on the wrapper instead of the the object. The novice might try to re-implement each method in the wrapper, just to call through to the underlying object. Now, if you consider wrapping an object with even a reasonable number of methods, you'll probably shudder (like I do) at the very thought. Besides being a waste of typing, this would bind a wrapper only to a given object, and we'd have to write a different wrapper for every class we want to wrap. This is definitely a design no-no, even if you're programming in Java. ;-)

As it turns out, Objective-C has some really cool functionality that would make a generic wrapper pretty trivial to write. The Objective-C runtime is extremely dynamic and powerful, and among other things, it allows you to do things with messages that you can't do with normal method calls, such as intercept, reinterpret, discard, forward, etc. Read more about it here.

For the simple message forwarding we want, we only have to implement two methods (-methodSignatureForSelector: and forwardInvocation:), but for good measure, we'll implement -respondsToSelector: so direct queries to the wrapper respond as the object itself would. Here's a simple class that wraps a Cocoa object, and whenever a message is sent to it, protects the underlying method call using an NSLock.

#import <Foundation/Foundation.h>
 
@interface AtomicMethodWrapper : NSObject
{
    id object;
    NSLock* lock;
}
 
- (id) initWithObject:(id)anObject;
 
@end

#import "AtomicMethodWrapper.h"
 
@implementation AtomicMethodWrapper
 
- (void) dealloc {
	[object release];
	[lock release];
	[super dealloc];
}
 
- (id) init {
	return [self initWithObject:nil];
}
 
- (id) initWithObject:(id)anObject {
    if ([super init] == nil) return nil;
    if (anObject == nil) {
    	[NSException raise:NSInternalInconsistencyException
	                format:@"[ %@ %s] -- Must provide a non-nil object.",
	        [self class], sel_getName(_cmd)];
    }
    object = [anObject retain];
    lock = [[NSLock alloc] init];
    return self;
}
 
// See documentation for -[NSObject respondsToSelector:]
- (BOOL) respondsToSelector:(SEL)aSelector {
    return [object respondsToSelector:aSelector];
}
 
// See documentation for -[NSObject methodSignatureForSelector:]
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [object methodSignatureForSelector:aSelector];
}
 
// See documentation for -[NSObject forwardInvocation:]
- (void) forwardInvocation:(NSInvocation *)anInvocation {
    [lock lock];
    [anInvocation invokeWithTarget:object];
    [lock unlock];
}
 
@end

This has the effect of making sure that each method call is atomic (meaning that only one thread may execute a method at once). Essecially, this means that, if everyone calls through the wrapper class, nobody can add or remove while you're doing a search, or vice versa.

However, there are two major shortcomings to this approach:

(1) Only single method calls are atomic — trying to use multiple methods in sequence without interference from other threads is still not possible.

(2) There is A LOT of runtime overhead involved with each method call. Since the wrapper class doesn't actually implement any of the methods which the wrapped object does, the runtime has to call -methodSignatureForSelector so it can marshall an appropriate NSInvocation, then send that to -forwardInvocation, which turns around and specifies the new target, at which point the runtime sends the message from the invocation to the underlying object. Very cool and dynamic, but exceedingly heavy-handed and slow for most purposes.

The "Reasonably Smart" Wrapper Approach

We can solve the first problem with some slight modifications that expose a few additional methods for the wrapper class. Conveniently, two of these methods (-lock and -unlock) are part of the NSLocking protocol, the same protocol adopted by NSLock and friends. We'll also add -tryLock (which NSLock also provides) to help users prevent deadlock. Here is the modified code:

#import <Foundation/Foundation.h>
 
@interface LockableObjectWrapper : NSObject <NSLocking>
{
    id object;
    NSLock* lock;
}
 
- (id) initWithObject:(id)anObject;
 
- (BOOL) tryLock;
// -lock and -unlock declared in NSLocking protocol
 
@end

#import "LockableObjectWrapper.h"
 
@implementation LockableObjectWrapper
 
- (void) dealloc {
	[object release];
	[lock release];
	[super dealloc];
}
 
- (id) init {
	return [self initWithObject:nil];
}
 
- (id) initWithObject:(id)anObject {
    if ([super init] == nil) return nil;
    if (anObject == nil) {
    	[NSException raise:NSInternalInconsistencyException
	                format:@"[ %@ %s] -- Must provide a non-nil object.",
	        [self class], sel_getName(_cmd)];
    }
    object = [anObject retain];
    lock = [[NSLock alloc] init];
    return self;
}
 
- (BOOL) tryLock {
    return [lock tryLock];
}
 
- (void) lock {
    [lock lock];
}
 
- (void) unlock {
    [lock unlock];
}
 
// See documentation for -[NSObject respondsToSelector:]
- (BOOL) respondsToSelector:(SEL)aSelector {
    return [object respondsToSelector:aSelector];
}
 
// See documentation for -[NSObject methodSignatureForSelector:]
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    return [object methodSignatureForSelector:aSelector];
}
 
// See documentation for -[NSObject forwardInvocation:]
- (void) forwardInvocation:(NSInvocation *)anInvocation {
    [anInvocation invokeWithTarget:object];
}
 
@end

This is a more versatile approach, since it allows users to manually lock and unlock a resource and create their own "critical section" of code that must execute without any interference from other threads. For example, with this approach, you can lock a mutable array, retrieve it's count, and remove some of the objects, move objects around, etc., then unlock it, all without fear that someone else will mess with it at the same time you are.

I call this approach "reasonably smart" because, although it automatically "knows" how to respond to messages, the overhead of message forwarding is still pretty heavy overkill from a runtime perspective (as cool as it is). Even so, sometimes it's a passable option. This code is academically interesting, but often impractical in actual use.

Remember, all of this could easily be accomplished with just the object and an NSLock, without wrapping it at all. However, if the goal is simplifying things for the users, and not having to remember to pass both the object and the lock, this can be a handy convenience. Besides, we needed an excuse to play around with message forwarding, right?

Summary

Multi-threading is a complex, multi-faceted topic, but the bulk of it boils down to one principle: you must be extremely careful when multiple threads need to access the same resource concurrently. Be sure that everyone plays by the rules and coordinates their actions on shared resources by synchronizing using mutexes, semaphores, or something similar.

Every synchronization method has its pros and cons. There is generally a tradeoff between speed for the program and simplicity for the programmer. As a rule of thumb, gauge the overhead of synchronization against the amount of work you have to do in the synchronized section. If performance is important (as it usually is when you're multi-threading) you should lean towards the simplest and fastest alternatives.

Your rating: None Average: 3.3 (3 votes)

Search

UPDATES

The site has recently been updated. You may notice some of the following issues:

  • Some URLs no longer work. Please use the search box.
  • File uploads should now be working. If you experience problems, please contact us.