Confine ALL the things!

I was talking with Saul Mora at lunchtime about NSManagedObjectContext thread confinement. We launched into an interesting thought experiment: what if every object ran on its own thread?

This would be interesting. You can never use a method that returns a value, because then you’d need to block this object’s thread while waiting for that object. You’d need to rely solely on being able to pass messages from one object to another, without seeing return values. In other words, the “tell don’t ask” principle is enforced.

You get to avoid a lot of problems with concurrency issues in any app. Each object has its own data (something we’ve been able to get right for decades), and its own context to work on that data. No object should be able to trample on another object’s data because we have encapsulation, and that means that no thread should be able to trample on another thread’s data because one thread == one object.

Also notice that one object == one thread. You don’t have to worry about whether different methods on the same object are thread-safe, because you’re confining the object to a single thread so no code in that object will execute in parallel.

So yeah, I built that thing. Or really I built a very simple test app that demonstrates that thing in action. Check out the source from the GitHub project linked here. For your edification, here’s a description of the meat of the confinement behaviour, the FZAConfinementProxy. Starting with the interface:

@interface FZAConfinementProxy : NSProxy

- (id)initWithRemoteObject: (id)confinedObject;

@end

This class is a NSProxy subclass, because all it needs to do is forward messages to another object. Or rather, that’s what it claims to do.

static const void *FZAProxyAssociation = (const void *)@"FZAConfinementProxyAssociationName";

@implementation FZAConfinementProxy {
    id remoteObject;
    NSOperationQueue *operationQueue;
}

- (id)initWithRemoteObject:(id)confinedObject {
    id existingProxy = objc_getAssociatedObject(confinedObject, FZAProxyAssociation);
    if (existingProxy) {
        self = nil;
        return existingProxy;
    }

Ensure that there’s exactly one proxy per object, using Objective-C’s associated objects to map proxies onto implementations.

    remoteObject = confinedObject;
    if ([remoteObject isKindOfClass: [UIView class]]) {
        operationQueue = [NSOperationQueue mainQueue];
    } else {
        operationQueue = [[NSOperationQueue alloc] init];
        operationQueue.maxConcurrentOperationCount = 1;
    }
    objc_setAssociatedObject(confinedObject, FZAProxyAssociation, self, OBJC_ASSOCIATION_ASSIGN);
    return self;
}

If we need to create a new proxy, then create an operation queue to which it can dispatch messages. UIKit needs to be used on the main thread, so ensure proxies for UIViews use the main operation queue.

What this means is that if your controller uses confinement proxies for both views and models (just as the dummy app does), then there’s no need to mess with methods like -performSelectorOnMainThread:withObject:waitUntilDone: in your controller logic. That’s automatically handled by the proxies.

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
    if (sel == @selector(initWithRemoteObject:)) {
        return [NSMethodSignature signatureWithObjCTypes: "@@:@"];
    }
    else {
        return [remoteObject methodSignatureForSelector: sel];
    }
}

Ensure that the Objective-C machinery can tell what messages it can send to this proxy.

- (void)forwardInvocation:(NSInvocation *)invocation {
    [invocation setTarget: remoteObject];
    NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithInvocation: invocation];
    [operationQueue addOperation: operation];
}

Here’s all the smarts, and it isn’t really very smart. Objective-C notices that the proxy object itself doesn’t implement the received method, and so asks the proxy whether it wants to forward the method. It’s, well, it’s a proxy, so it does want to. It rewrites the invocation target to point to the confined object, then adds the invocation to the operation queue.

That’s really it, bar some cleanup that you can see in GitHub. One thread per object limits the ways in which you can design code, so it’s a great way to learn about writing code within those limitations.

About Graham

I make it faster and easier for you to create high-quality code.
This entry was posted in code-level, software-engineering. Bookmark the permalink.

4 Responses to Confine ALL the things!

  1. Jeff says:

    Interesting idea. BTW, this seems to confine objects to queues, however queues don’t correlate neatly to threads. Is there something I’m missing about how objects are confined to threads, or is queue-confining enough?

  2. Graham says:

    Jeff, what this does is guarantee that an object is executing at most one concurrent message, that it’s on _some_ thread that you don’t control, and that it’s the only thing on that thread (except for UI objects which are all on the main thread). That enforces enough thread confinement to make you consider each object as a separate actor. As an aside, it also means you’re not allowed to think about “this object is local to _this_ thread” which may or may not be a good thing.

  3. James says:

    This is interesting. One problem is I don’t find NSProxy plays nice with UIView when you’re manipulating the view hierarchies.

    To test it out, re-add the self.salaryField to self.view on viewDidLoad causes uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[UITextField superlayer], maybe there’s anyway to fix it or maybe I shouldn’t do it anyway.

  4. Graham says:

    James, you should only use confinement proxies for your own messages. You can’t control how other peoples’ frameworks like UIKit are going to message objects, so (as you’ve found) the constraints imposed by a system like confinement proxies won’t necessarily be satisfied.

    The way I use confinement proxies is to confine my own controller and model objects to their own queues, and to enforce a tell-don’t-ask messaging scheme between those objects and the views. Where my controllers need to message the views, they do it via confinement proxies – which automatically push all messages back to the main thread. When I need to configure UIKit, that’s done accessing objects directly.

Comments are closed.