test and set atomic flags, Swift 3/iOS 10

Hi,


I am resurrecting a camera project, and need to update the OSAtomicTestAndSet method I was using in Swift 2/iOS9. Snipped code below. The instance method continuously recieves new video frames, and I want to 'do stuff' to one frame at any given time. If a frame is being processed I want to ignore any subsequently queued frames. Once a frame is processed I want to pull in the next available frame after that time point (ie not the next one in the frame queue). This is why I am setting 'self.processingFrame=0' on the video frame capture queue, so I will keep ignoring any frames queued before 'now'.


Currently I only get deprecation warnings for Swift3, that I should update to atomic_fetch_or_explicit(). Is there a more Swift-like way to implement this, or should I persist with C-like atomic thinking? Both in terms of better code, and future-proofing (Swift 4/iOS 11+).



class ViewController: UIViewController, AVCaptureVideoDataOutputSampleBufferDelegate, AVCapturePhotoCaptureDelegate {

    fileprivate var processingFrame = 0

    // instance method of AVCaptureVideoDataOutputSampleBufferDelegate, receives frames from video session
    func captureOutput(_ captureOutput: AVCaptureOutput!, didOutputSampleBuffer sampleBuffer: CMSampleBuffer!, from connection: AVCaptureConnection!) {
        // Only want to process one frame at a time
        if(OSAtomicTestAndSet(0, &processingFrame) == false) {

            // do stuff to this frame

            // execute this via the video frame queue so we don't process any frames delivered before
            // this frame was finished processing
            self.captureSessionFrameQueue.async(execute: {
                self.processingFrame = 0
            })
        }
    }
}



cheers,

Accepted Reply

Is there a more Swift-like way to implement this, or should I persist with C-like atomic thinking?

The problem here is that Swift doesn’t have a concurrency-aware memory model. Without that, it’s very hard to offer any concrete guarantees about what will or won’t work in the long term.

This situation is unlikely to change in the next year or so. The Swift folks just kicked off the Swift 5 effort and concurrency is not on that particular radar.

Until that situation is resolved my recommendation is that you avoid doing ‘clever’ things with concurrency. And in this context ‘clever’ includes anything to do with lock-free data structures.

Note In my experience folks reach for lock-free data structures way too much. While they do have their place, it’s rare that they offer significant benefits that you can’t get otherwise.

Now, in your specific case an atomic test and set is the simplest lock-free data structure, so it’s hard to argue against it. It still makes me kinda nervous though. In your shoes I’d probably do one of two things:

  • Build an abstraction around

    processingFrame
    in a language that does have a memory model (that is, something C based) and then call that from Swift.
  • Switch to a locked data structure.

With regards the last point, it’s easy to implement a test-and-set operation using a dispatch semaphore. Alternatively, you could add a serial queue to your view controller and use it to guard

processingFrame
:
class ViewController … {

    var processingFrame = false
    var serialQueue = …

    func captureOutput(…) {  
        self.serialQueue.async {
            if !self.processingFrame {
                self.processingFrame = true
                self.captureSessionFrameQueue.async {
                    … do the work …
                    self.serialQueue.async {
                        self.processingFrame = false
                    }
                }
            }
        }
    }
}

The amount of time you spend running on

serialQueue
is so short that there’s a very low chance of any contention.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

Is there a more Swift-like way to implement this, or should I persist with C-like atomic thinking?

The problem here is that Swift doesn’t have a concurrency-aware memory model. Without that, it’s very hard to offer any concrete guarantees about what will or won’t work in the long term.

This situation is unlikely to change in the next year or so. The Swift folks just kicked off the Swift 5 effort and concurrency is not on that particular radar.

Until that situation is resolved my recommendation is that you avoid doing ‘clever’ things with concurrency. And in this context ‘clever’ includes anything to do with lock-free data structures.

Note In my experience folks reach for lock-free data structures way too much. While they do have their place, it’s rare that they offer significant benefits that you can’t get otherwise.

Now, in your specific case an atomic test and set is the simplest lock-free data structure, so it’s hard to argue against it. It still makes me kinda nervous though. In your shoes I’d probably do one of two things:

  • Build an abstraction around

    processingFrame
    in a language that does have a memory model (that is, something C based) and then call that from Swift.
  • Switch to a locked data structure.

With regards the last point, it’s easy to implement a test-and-set operation using a dispatch semaphore. Alternatively, you could add a serial queue to your view controller and use it to guard

processingFrame
:
class ViewController … {

    var processingFrame = false
    var serialQueue = …

    func captureOutput(…) {  
        self.serialQueue.async {
            if !self.processingFrame {
                self.processingFrame = true
                self.captureSessionFrameQueue.async {
                    … do the work …
                    self.serialQueue.async {
                        self.processingFrame = false
                    }
                }
            }
        }
    }
}

The amount of time you spend running on

serialQueue
is so short that there’s a very low chance of any contention.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi,


Thanks for your detailed answer. It feels like OSAtomicTestAndSet is exactly the fit I'm after, unfortunately. Which is check this flag and only run if it's not set, so 'mostly don't execute anything, and don't block while you're at it.' It seems most Swift built-ins are of the blocking variety, designed to execute multiple tasks safely. It may (or may not) be a subtle difference, but I want to not execute multiple tasks safely. 🙂


So, I tried out NSlock.try() which although non-blocking, seems to introduce a potential negative side effect depending on which thread the lock executes on. And then I tried a queue implementation, which feels better (than NSLock). I use two queues to avoid blocking while actually processing the frame, but want to 'release' the variable on the external video frame queue.


So I was thinking I'll go with the double-queue implementation. I'm running at 30fps (video) at the moment, so the time difference from OSAtomic shouldn't really have an impact. I added in a little check to see if any iterations were being skipped in the queue version, and was a little surprised that more than 50% were being skipped due to the time taken for processingFrame=0 to finally execute. I'll have to run this on a device (iPhone) and see what effect there is when trying to process video frames.


I haven't really gone into mixing C and Swift before (as an abstraction around processingFrame), but perhaps that's where I'm headed in the longer term. Although at that point I guess I'd just be re-implementing OSAtomicTestAndSet.



/ /: Playground - noun: a place where people can play
import Foundation

// simulated external queue, this is really the AVCaptureSession delegate queue
let captureSessionFrameQueue = DispatchQueue(label: "my_video_frame_q")

func atomic(count: Int)->Double {
    var processingFrame = 0
    let start_time = Date()
    for _ in 0...count {
        if(OSAtomicTestAndSet(0, &processingFrame) == false) {
            processingFrame = 0
        }
    }
    let time_taken = Date().timeIntervalSince(start_time)
    return time_taken
}

func locking(count: Int)->Double {
    let myLock = NSLock()
    let start_time = Date()
    for _ in 0...count {
        if (myLock.try()) {
            myLock.unlock()
        }
    }
    let time_taken = Date().timeIntervalSince(start_time)
    return time_taken
}

func serial_queue(count: Int)->Double {
    let mySerialQueue = DispatchQueue(label: "my_serial_q")
    let myFrameProcessingQueue = DispatchQueue(label: "my_frame_processing_q")
    var processingFrame = 0
    var ignored = 0
    let start_time = Date()

    for _ in 0...count {
        mySerialQueue.async {
            if processingFrame == 0 {
                processingFrame = 1
                myFrameProcessingQueue.async {
                    // do work on current video frame, but don't block mySerialQueue
             
                    // must 'release' processingFrame on captureSessionFrameQueue
                    // so it executes after any currently queued video frames
                    captureSessionFrameQueue.async(execute: {
                        processingFrame = 0
                    })
                }
            }
            else {
                ignored += 1
            }
        }
    }
    let time_taken = Date().timeIntervalSince(start_time)
    print("serial_queue processed \(count-ignored) and ignored \(ignored)")
    return time_taken
}

let iterations = 100000
for _ in 1...5 {
    print(atomic(count: iterations), locking(count: iterations), serial_queue(count: iterations))
}


results:

serial_queue processed 41581 and ignored 58365

5.79971098899841 15.9877420067787 31.2861160039902

serial_queue processed 41906 and ignored 58095

5.50851899385452 16.1453329920769 32.2434309720993

serial_queue processed 41583 and ignored 58417

5.50454598665237 15.3047040104866 31.2974920272827

serial_queue processed 41883 and ignored 58118

5.82396399974823 15.4081450104713 28.7865089774132

serial_queue processed 43892 and ignored 56108

5.36022198200226 13.5767689943314 29.1582999825478


cheers,

I'm running at 30fps (video) at the moment, so the time difference from OSAtomic shouldn't really have an impact.

Agreed.

I'll have to run this on a device (iPhone) and see what effect there is when trying to process video frames.

Right, because your microbenchmark isn’t really valid here. Processing a frame is sufficiently expensive that the difference between the two synchronisation techniques will disappear in the noise (or at least I expect that’s the case). However, if you remove the “processing a frame” part then all you’re doing is measuring the synchronisation cost. It’s obvious that atomic test-and-set is going to win that battle, but that’s not really the question you’re trying to answer.

Did you try using a dispatch semaphore? That seems like it’d offer a good balance between performance and easy of use.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Hi,


Yes, I've just tried the semaphore method. I had thought that it was (sort of by definition) blocking, but didn't realise that you can avoid that with a timeout of 'now'. As you say, this is maybe the best middle-way between functionality and performance.


thanks for your help, it's good to run through all the options and then pick the most likely to pursue further.


cheers,


class myController {
  
    let myLock = NSLock()
    let mySemaphore = DispatchSemaphore(value: 1)

    func bySempahores() {
        if (mySemaphore.wait(timeout: DispatchTime.now()) == DispatchTimeoutResult.success) {
            doWorkOnFrame()
          
            captureSessionFrameQueue.async {
                self.mySemaphore.signal()
            }
        }
    }
    func byLocking() {
        if(myLock.try()) {
            doWorkOnFrame()
          
            captureSessionFrameQueue.async {
                self.myLock.unlock()
            }
        }
    }
    func doWorkOnFrame() { }
}