Bindings with a threaded app.. is this crazy?

I know AppKit isn't thread safe and this may seem like an obvious thing in hindsight but I was expecting cocoa bindings controllers to dispatch updates to UI elements in the main thread. I was clearly wrong in that assumption.


In thinking about this problem, I started to play around with a controller object that would do exactly that... proxy observations but notify it's observers on the main thread. I put together a POC (I'm calling it AirLock for now) and it seems to work, but it was so easy that I'm nervous that I've overlooked something obvious.


The setup is simple. If a control was bound to SomeObject.someKeyPath:

o In IB, create an intance of AirLock and set the represented object to SomeObject

o Change the binding of the control to AirLock.representedObject.someKeyPath


This is the code for AirLock. Is there a better way?


@interface AirLock : NSObject
@property (nonatomic, retain) IBOutlet id representedObject;
@end



@interface AirLockContext : NSObject
@property (nonatomic, retain) NSObject *observer;
@property (nonatomic, retain) NSString *keyPath;
@property (nonatomic) void *context;
@end

@implementation AirLockContext
@end

@implementation AirLock
- (void)addObserver:(NSObject *)observer
         forKeyPath:(NSString *)keyPath
            options:(NSKeyValueObservingOptions)options
            context:(void *)context
{
    if ([keyPath hasPrefix:@"representedObject."])
    {
        NSString *subPath = [keyPath substringFromIndex:@"representedObject.".length];
        AirLockContext *ctx = [[AirLockContext alloc] init];
        ctx.observer = observer;
        ctx.keyPath = keyPath;
        ctx.context = context;

        [_representedObject addObserver:self forKeyPath:subPath options:options context:(void *)CFBridgingRetain(ctx)];
    }
    else
    {
        [super addObserver:observer forKeyPath:keyPath options:options context:context];
    }
}

- (void)removeObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath context:(void *)context
{
    if ([keyPath hasPrefix:@"representedObject."])
    {
        // TODO
    }
    else
    {
        [super removeObserver:observer forKeyPath:keyPath context:context];
    }
}

- (void)observeValueForKeyPath:(NSString *)keyPath
                      ofObject:(id)object
                        change:(NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(void *)context
{
    AirLockContext *ctx = (__bridge AirLockContext *)context;

    dispatch_async(dispatch_get_main_queue(), ^{
      [ctx.observer observeValueForKeyPath:ctx.keyPath ofObject:self change:change context:ctx.context];
    });
}
@end

I think the short answer is that this is not thread-safe. The pattern you've used to move the notification onto the main thread is called a "trampoline" and that seems fine (though you could perhaps test if you're already on the main thread and avoid the dispatch if you are).


However, a binding is also used to retrieve and update the values of properties of the bound object. An update from the UI will always happen on the main thread. However, if you needed this AirLock object at all, it's because you expect that something else is updating the properties on a background thread. That opens up the possibility of more or less simultaneous updates on two threads, which isn't safe. Even if the UI element is read only, it's not thread safe (in general) to read a value on one thread that might be mutated on another thread.


A secondary problem, I think, is that a UI with lots of bound elements is going to need a lot of AirLock objects in the storyboard or XIB. It seems like a housekeeping nightmare to keep them all straight, and use the correct object for the correct UI element.


IMO, a good rule of thumb is that there is no generally applicable pattern to solve thread safety issues. Sad to say, (again IMO) if you think you've found an easy way, you have to assume you're wrong.

So let me clarify.. This code does *not* attempt to solve any muti-threading issues at the model level. The model should be made thread safe no matter what this code does, so using the main UI thread (or any other thread) to update my model will be fine. Exactly as you pointed out, the only need for this object is to deal with background threads notifying UI bindings into AppKit.


As for needing many instances, that all depends on how many root objects exist in the nib. In the simplest scenario, everything binds through the delegate or file owner, in which case I only need 1 instance of AirLock for the entire nib. You mentioned storyboards, which my app does not use, so maybe there's an idiosyncrasy that I'm not aware of?


Without something like this, it seems like cocoa bindings should not be used in any app with more than 1 thread.

This code does what it does (correctly), but the need to trampoline KVO notifications across thread boundaries implies that a thread safety issue might exist anyway.


>> The model should be made thread safe no matter what this code does


If your app is already designed to access the data model from background threads safely (e.g. by using a lock), then, yes, the AirLock class would be sufficient to transfer just the notifications to the main thread. In that sense, the answer to your question is, no, you haven't overlooked anything obvious.

However, it's not IMO very palatable. An app design where KVO notifications for data model properties being used directly by the UI are delivered on a background thread seems — to say the least — incomplete.

So what is the palatable solution? I must be missing something. Don't use bindings or never mutate any object in other than the main thread? Both seem pretty unpalatable.

I don't think there's any general methodology, that's my point.


What I'd try to do, if possible, is to make the background thread accesses to the data model be safe by using dispatch_async or dispatch_sync to the main queue, which would take care of the KVO notification problem automatically. The whole thing becomes very simple and efficient (since the dispatches are likely to perform better than locks).


I can imagine situations where you wouldn't want to use this approach. For example, if the background thread needs to go on accessing the latest data model (in subsequent code) and so needs the value to have been updated synchronously, but if dispatch_sync risks too big a delay (since you don't know exactly what else might get hold of the main thread and for how long).


In that case, I'd probably arrange to suppress the automatic KVO notifications for the properties exposed to the UI, and generate the notifications manually using the "willChangeValueForKey:" and "didChangeValueForKey:" methods within a block that was sent to the main thread via dispatch_async. More specifically, I'd probably encapsulate this behavior in a method of the data model for each property. There are other solutions, too.


Certainly, your AirLock technique achieves exactly the same result as that. My reluctance would be that I couldn't trust myself (and certainly wouldn't trust other people coding or designing UI against the same data model) to remember to install the correct AirLock without making any mistakes. It seems to me that a complete app design would solve the problem internally without exposing its vulnerability to the outside world.

I see your points and I'll probably take some time to digest it all. One thing strikes me right off the bat.


Putting the burden of UI thread safety on the model seems wrong. I don't just mean data model, I mean the entirety of the M (in MVC) layer. I like to imagine I'm writing a tic-tac-toe class that might be feeding a cocoa UI or html or web services or whatever. Why should that code care about such things?


Consider AVPlayer for a minute. It has a rate property, which I believe is observable and thus bindable. When the movie is over, AVPlayer will set that rate to zero (that's from my memory). Are we saying the Apple absolutely positively sets that property to zero in the main thread? Does everyone do this?


Thank you for all of your patient and detailed answers.

>> Why should that code care about such things?


It would depend on the code. Some code might expect use across thread boundaries, and choose to be as robust as possible. Other code might expect to be used in multiple scenarios, and might refrain from building in the thread safety overheads.


>> Consider AVPlayer for a minute.


As it turns out, a wonderful example. I went to the class documentation (developer.apple.com/reference/avfoundation/avplayer) and found this:


"You can use Key-value observing (KVO) to observe state changes to many of the player’s dynamic properties, such as its currentItem or its playback rate. You should register and unregister for KVO change notifications on the main thread. This avoids the possibility of receiving a partial notification if a change is being made on another thread. AV Foundation invokes observeValue(forKeyPath:of:change:context:) on the main thread, even if the change operation is made on another thread."


By contrast, look at AVAsynchronousKeyValueLoading, used by AVAsset subclasses. It turns the problem around by using completion handlers instead of individual property notifications, providing a unified model that's easier to use than per-property API contracts, without placing any (direct) main thread requirements on client code. (But if you want to bind UI elements to these resource values, you'll need to provide your own thread-safety solution for that part of the problem.)


AVFoundation is one framework where APIs are carefully designed to avoid dumping the safety burden entirely on the client.

Annotating this thread subsequent to the transition to Apple Silicon, which is basically complete at the time of this writing. I think the methodology proposed at the top of this discussion is a workable and effective strategy for dealing with this problem which is going to become more and more pervasive. Many new SDKs from Apple will be thread-safe and capable of generating KVO notifications on any thread or queue. However, I think it unlikely at AppKit and UIKit will be thread safe. And there's the challenge of supporting the widest array of Mac hardware.

The Objective-C runtime already has solutions for this problem which date back to a technology called Distributed Objects. Not much has been said about this tech for a long while because security concerns promoted XPC to the foreground but XPC doesn't really provide the same functionality.

The point here is that the NSProxy NSInvocation classes and patterns can be used to "remote" almost any Objective-C method call, including over thread boundaries, process boundaries and to remote machines on the LAN or interweb. Check out NSDistantObject for some perspective.

You can build a controller layer whose sole purpose is to proxy all KVO notifications onto the main thread to AppKit.

Take this sample code from the archive for example: https://developer.apple.com/library/archive/samplecode/AVRecorder/Introduction/Intro.html#//apple_ref/doc/uid/DTS40011004

I have refactored and referred to this sample several times; it's an excellent sample. But as of 2023, the KVO bindings from the UI are not properly working. Exceptions are being thrown and KVO notifications lost, leading to indeterminate states of the UI. Maybe these are bugs in AppKit that will be remedied (sometime in the future).

However, I was easily able to solve the problems with this sample by building a controller layer between AppKit and AVCaptureDevice et al. This was before I found NSInvocation and basically I am dispatching to the main thread. My solution is just a simple proxy object that forwards all the valueForKeyPath type methods to the target objects (I have one controller bound to all the various AVCaptureDevice type key paths). It's a very simple class and has restored this sample code to its original lustre and glory. But it could be even simpler next time I revisit the code:

For my next Cocoa nightmare I dove deeper into NSInvocation and learned that you can completely remote an entire class with just four Objective-C methods. Check out the docs for methodSignatureForSelector: and go down the rabbit hole:

from NSInvocation:

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
- (void)invokeWithTarget:(id)target;
- (void)forwardInvocation:(NSInvocation *)invocation;
- (id)forwardingTargetForSelector:(SEL)methodSelector;`

You'll get warnings from a modern Objective-C compiler so declare the exposed keypaths/properties as usual and mark them as @dynamic so the compiler doesn't synthesize methods for them. Once you get to googling NSInvocation and any of the four methods listed above, I think you'll find much has been written on this subject going back to Panther and even OpenStep.

https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/DistrObjects/DistrObjects.html

Bindings with a threaded app.. is this crazy?
 
 
Q