Duck
Duck

Reputation: 35953

Failing to see how @synchronized works with asynchronous blocks

Suppose I have a function like the following one, that is called 30 times per second (this code is a simplification of a more complex code created by Apple that I am trying to understand. The idea of Apple's original code is this: like I said doSomethingWithImage is called 30 per second. So, the whole method has 1/30 of a second to do everything but imagine that doSomethingThatWillTakeTime takes more than expected and doSomethingWithImage is called again, while doSomethingThatWillTakeTime is already being executed. If this is the case, the code has to drop that image and do nothing.

// code...
@synchronized (self);
  [self doSomethingWithImage:image];
}

// ...

// call under @synchronized( self )
- (void)doSomethingWithImage:(UIImage *)image
{
    self.myImage = image;

    [self executeBlockAsync:^{ 

        UIImage *myImage = nil;

        @synchronized( self ) 
        {
            myImage = self.myImage; 
            if (myImage) { 
                self.myImage = nil; // drop the image
            }
        }

        if ( myImage ) { 
            [self doSomethingThatWillTakeTime:myImage];  
        }
    }];
}


 (void)executeBlockAsync:(dispatch_block_t)block
{
    dispatch_async( _myQueue, ^{
            callbackBlock();
    } );
}

My problem is that looking at this code I cannot see how this is doing that. I do not have a mental image of what lines are being execute in which order for that to occur. Probably I do not understand the role of @synchronized in doing that, on what exactly is being locked or not.

Upvotes: 1

Views: 101

Answers (3)

Rob
Rob

Reputation: 437482

The only thing that @synchronized does is to ensure "thread safe", synchronized interaction with some objects or variables. Specifically, it ensures that you don't access these resources from one thread while they're possibly being mutated elsewhere by another thread. If one thread is inside the @synchronized(self) {...} block and another thread encounters its own @synchronized(self) {...} block, this latter code won't start running that until the other thread finishes its @synchronized block.

This is just one of the many synchronization techniques outlined in the Threading Programming Guide: Synchronization. (Or you can also use dedicated GCD queues, either serial queues or concurrent queues that use the reader-writer pattern, to manage this synchronization as outlined in Concurrency Programming Guide: Migrating Away from Threads: Eliminating Lock-based Code. But that's a separate topic altogether.)

In the captureOutput:didOutputSampleBuffer:fromConnection: method that you shared with us, they're just using @synchronized to ensure that the _recordingStatus and the _recorder don't change in one thread while you're using them from another thread. They accomplish this by making sure that all interactions with these variables (whether reading in one thread or updating in another) happens within their own @synchronized { ... } blocks.

But note that @synchronized does not serve any functional purpose beyond that thread safe interaction. All of the logic regarding "should I add a frame or not" is dictated by the simple if statements in that code. The @synchronized directives have nothing to do with that. They're just there to ensure thread safety.

Upvotes: 2

Paulw11
Paulw11

Reputation: 114836

It isn't that the code "drops the image and does nothing". The use of the term "drop" is probably a bit misleading; what happens is that images that haven't yet been delivered to the delegate are overwritten if there is a new image.

What happens is something like this:

  • Image 1 is stored in self.myImage
  • An asynchronous task is enqueued on a serial dispatch queue to process Image 1.
  • When this task executes it picks up the current self.myImage and then clears self.myImage
  • If a non-nil image was picked up, it is passed to doSomethingThatWillTakeTime

Later,

  • Image 2 is stored in self.myImage
  • An asynchronous task is enqueued on a serial dispatch queue to process Image 2
  • Since it is a serial dispatch queue, if the first task isn't done, this task will wait.

Now say,

  • Image 3 is stored in self.myImage - note that this doesn't check to see if self.myImage is nil, it just overwrites it, so if Image 2 was never processed it has been "dropped"
  • An asynchronous task is enqueued on a serial dispatch queue to process Image 3, but it also has to wait
  • Now, when the first task finishes, the second will execute. It will pick up the value from self.myImage which is now Image 3 and clear self.myImage
  • Image 3 is passed to doSomethingThatWillTakeTime
  • When the second task completes, the third task will start.
  • It will get nil from self.myImage do nothing.

So you can see how Image 2 was dropped since it was stale; Image 3 had already arrived, so there was no point in processing Image 2.

Essentially, this code implements a circular buffer of size 1

Upvotes: 2

Codo
Codo

Reputation: 78825

The crucial thing is executeBlockAsync, which in turn calls dispatch_async. So the real work is dispatched to a background queue, which is most likely configured to be working sequentially. doSomethingWithImage immediately returns and will never take 1/30s.

So the dispatching guarantees that image processing is done serially. This works as long as image processing doesn't take more than 1/30s on average. Single images can take longer. If it persists to take longer, the queue will fill up.

I don't understand what @synchronized does in this code. It only makes sense if there is more code (not shown here) that uses the same @synchronized block.

Upvotes: 0

Related Questions