Errata: Timer has race condition that prevents invalidation


#1

Under “NSTimer-based Animation” the code contains a major flaw that can easily be fixed.

When doubleclicking the button in quick succession, a new rolling-cycle is started and sometimes the previous timer is repeating endlessly (never gets invalidated) when the rollsRemaining property is decremented below 0. The culprit is the usage of shared state between the timers, which is a big NO-NO and shouldn’t be taught in the book! Seriously…

Ugly Fix:

change the if condition in rollTick(_:slight_smile: to

Better:
Use the userInfo dictionary to give every timer its own running counter for example.


#2

I’m not able to see your proposed solutions. Will you repost them? Thanks.


#3

The good news is that because timer events scheduled on main thread are delivered to the main thread, race conditions on shared state are impossible to occur.

The following code can run multiple timers concurrently, each timer event modifying the same variable.

//
//  AppDelegate.swift
//  Timer
//
//  Created by ibex on 14/6/18.
//  Copyright © 2018 ibex10. All rights reserved.
//

import Cocoa

@NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {

    @IBOutlet weak var window: NSWindow!

    func applicationDidFinishLaunching(_ aNotification: Notification) {
        // Insert code here to initialize your application
    }

    func applicationWillTerminate(_ aNotification: Notification) {
        // Insert code here to tear down your application
    }

    func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
        return false
    }

    // -----------------------------------------------------
    //
    var sharedCounter = 100000
    let interval = 0.001
    
    // -----------------------------------------------------
    //
    @IBAction func startJibber (sender:NSButton) {
        print (#function)
        guard sharedCounter > 0 else {
            return
        }
        Timer.scheduledTimer (withTimeInterval: interval, repeats: true) {(timer:Timer) in
            assert (self.sharedCounter >= 0)

            if (self.sharedCounter > 0) {
                self.sharedCounter -= 1
            }
            if self.sharedCounter == 0 {
                print ("timer: \(timer): done")
                timer.invalidate()
            }
        }
    }
    
    // -----------------------------------------------------
    //
    @IBAction func startJabber (sender:NSButton) {
        print (#function)
        guard sharedCounter > 0 else {
            return
        }
        Timer.scheduledTimer (timeInterval: interval, target: self, selector: #selector (timerExpired (timer:)), userInfo: nil, repeats: true)
    }

    // -----------------------------------------------------
    //
    @objc func timerExpired (timer:Timer) {
        assert (sharedCounter >= 0)

        if (sharedCounter > 0) {
            sharedCounter -= 1
        }
        if sharedCounter == 0 {
            print ("timer: \(timer): done")
            timer.invalidate()
        }
    }
}