6
\$\begingroup\$

I thought it would be helpful to put together an example of what I'm talking about in the second part of my answer to this Stack Overflow question.

The idea is that there's some data that is updated very quickly (i.e. faster than the main queue can reasonably process blocks) on a background thread that needs to be displayed in a UI (in this code, a counter in a while loop). Instead of trying to update the UI at the same rate that the data is updated, a timer is setup on the main thread which fires off a method updating the UI every 0.1 seconds or so. Also taken in to account is the need for data synchronization between the writes on the background thread and the reads on the main thread. All threading and data synchronization is handled using Grand Central Dispatch.

My intent with this code is mainly to help teach someone how to go about this without a separate writeup explaining the code; it's meant to accompany the explanation of the concept in my answer. With that in mind, I put in a ton of comments attempting to explain what I'm doing and why I'm doing it, even when it seemed obvious. However, I tried to not repeat myself. Since this is written in Swift and the language is very new, there are also some comments explaining some things that need to be considered for Swift specifically.

What I'm looking for is just a general code review, keeping in mind that the code and comments are intended to try to help teach someone how to implement this concept.

@IBOutlet weak var dataTextField: NSTextField!
@IBOutlet weak var countTextField: NSTextField!

var data: UInt64 = 0
var uiUpdateCount: UInt64 = 0
var uiUpdateTimer: NSTimer? = nil

/* 
    How often to update the UI, in seconds.

    Feel free to play with this value and see what happens, 
    even 0.01 (~1/60th of a second) only uses ~4% CPU for 
    the UI thread on my machine.
*/
var updateInterval: NSTimeInterval = 0.1

// Create a serial queue for synchronized data access
let dataAccessQueue = dispatch_queue_create("uiUpdateTimerExample.dataAccessQueue", DISPATCH_QUEUE_SERIAL)

func startup() {
    // Start an infinite while loop to update our data on a background thread
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)) {
        /*
            Make sure self is marked as weak so that we don't create a strong reference cycle.
            See: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/AutomaticReferenceCounting.html#//apple_ref/doc/uid/TP40014097-CH20-XID_100
            Note: self will now be an optional, so could be nil.
        */
        [weak self] in

        while (true) {
            /*
                Use optional chaining to make sure the self.updateData() function can still be called.
                See: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/OptionalChaining.html#//apple_ref/doc/uid/TP40014097-CH21-XID_361
            */
            self?.updateData()
        }
    }

    // Start the UI update timer on the main queue
    dispatch_async(dispatch_get_main_queue()) {
        [weak self] in

        // Start the UI update timer; calls updateUI() once every updateInterval
        self?.uiUpdateTimer = NSTimer.scheduledTimerWithTimeInterval(self!.updateInterval, target: self!, selector: "updateUI", userInfo: nil, repeats: true)

        /*
            Since this closure only has one expression in it, we have to explicitly return.
            Otherwise we'll run in to problems with Swift's "Implicit Returns from Single-Expression Closures"
            See: https://developer.apple.com/library/prerelease/ios/documentation/Swift/Conceptual/Swift_Programming_Language/Closures.html#//apple_ref/doc/uid/TP40014097-CH11-XID_158
        */
        return
    }
}

func shutdown() {
    // Stop and release the UI update timer on the main queue
    dispatch_async(dispatch_get_main_queue()) {
        [weak self] in
        self?.uiUpdateTimer?.invalidate()
        self?.uiUpdateTimer = nil
    }
}

func updateData() {
    /*
        Dispatch a data update synchronously to the dataAccessQueue.
        Since dataAccessQueue is serial, it'll only run one code block at
        a time, in the order they're received. If we do all read and writes
        to our data in the dataAccessQueue, there shouldn't be any data
        contention issues.
    */
    dispatch_sync(self.dataAccessQueue) {
        [weak self] in

        // Our "data update" is just incrementing a counter
        self?.data += 1

        return
    }
}

func updateUI() {
    uiUpdateCount++

    // Dispatch the data read synchronously to the dataAccessQueue
    dispatch_sync(dataAccessQueue) {
        [weak self] in
        self?.dataTextField.stringValue = "\(self!.data)" // Update the data update count label
        return
    }

    /*
        Update the UI update count label outside the dataAccessQueue block;
        we don't want to do more in there than we have to.
    */
    self.countTextField.stringValue = "\(uiUpdateCount)"
}

For those that want to run the code but don't want to setup an entire project, the complete project is on GitHub.

\$\endgroup\$
1
  • 1
    \$\begingroup\$ This doesn't really fit in my answer, nor is it really worthy of its own answer, but if I see a variable called data, I expect it either to be NSData or an array of bytes. \$\endgroup\$
    – nhgrif
    Aug 27, 2014 at 0:20

1 Answer 1

3
\$\begingroup\$

I don't think this code is very good teaching code.


First of all, I personally prefer using NSOperationQueue for this sort of stuff. My guess is that people completely uncomfortable with asynchronous code will be also. NSOperationQueue code looks just like all your other Objective-C/Swift stuff. Yes, GCD definitely has its advantages, but it just doesn't quite fit in (from an appearance standpoint) with all the other rest of the code. I think people who need a tutorial instead of just reading documentation are more likely going to want something that looks comfortable and familiar.


Second... I don't like the idea that someone would be taught that there should be no synchronization between data updates and UI updates. Yes, your code does manage to update the UI on the main thread, and it does manage to update the data in the background thread.

But they each do this completely independently. It's nothing special. And you're right, even updating at 10 milliseconds, you don't use that much processing power.

But any time you update the UI when the data hasn't updated, you've wasted time on the processor. May not be a big deal, and in this case, it's definitely not, but this is an extraordinarily simplistic case.

Moreover, any time you update the data and don't immediately update the UI to reflect that change in data, your UI is behind.

The more correct way to handle this sort of stuff, in my opinion, isn't to have a looping updater that refreshes your entire UI every 10ms (consider a more complicated view with several more elements to update).

Just as we can from the main thread put code to run on a background thread, from a background thread, we can add code to the main queue. The appropriate approach then can only logically be to dispatch the UI update onto the main thread from the background thread when new data has actually been received.

We can use dispatch_async(dispatch_get_main_queue()) from a background thread to queue up executable code onto the main thread.

(Or, if we opt for NSOperationQueue's, we can call the mainQueue() method to get a reference to the main queue and add operations to it.)

\$\endgroup\$
7
  • \$\begingroup\$ I should have made one thing more clear: we're talking about data updates faster than the UI can reasonably refresh (hence my use of an infinite while loop to simulate very quick data updates). I completely agree that dispatching back to the main queue is the ideal way to do it (in fact, in my SO answer, I said to try that first), but that doesn't always work. If you make that change in the code I posted, for example, you fill up the main queue faster than it can handle and the UI hangs. That's also why I didn't use NSOperationQueue: I didn't want the overhead of creating NSOperations. \$\endgroup\$
    – Mike S
    Aug 27, 2014 at 0:27
  • \$\begingroup\$ Calling mainQueue has very little overhead as it's a shared instance. But regardless, it's extraordinarily simple, plus the ability to pass NSOperationQueue's around makes them quite powerful. You don't have to create an NSOperation to use an NSOperationQueue (addOperationWithBlock: ... though I'm not sure of the Swift equivalent). \$\endgroup\$
    – nhgrif
    Aug 27, 2014 at 0:30
  • 1
    \$\begingroup\$ But the real question is... if you're updating the data faster than you can update the UI, is the data actually updating? (If a tree falls in the woods, etc...) So to me, seems the real solution might be to slow down the data updates... \$\endgroup\$
    – nhgrif
    Aug 27, 2014 at 0:31
  • \$\begingroup\$ @MikeS Consider how I handle multi-threading in this library: github.com/nhgrif/SQLConnect in terms of thinking about NSOperationQueues. \$\endgroup\$
    – nhgrif
    Aug 27, 2014 at 0:36
  • \$\begingroup\$ Maybe I'm not understanding what you mean by slowing down data updates, but I don't think that is always possible. The data could be coming from an external source, or it could be a simulation that is constantly updating. (I want to stress that I agree with your answer 100% for the vast majority of cases, I just feel like there are cases where this concept is useful) \$\endgroup\$
    – Mike S
    Aug 27, 2014 at 0:57

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.