How to utilize NSLock to prevent a function from firing twice?
Asked Answered
G

3

14

I current have a set of asynchronous functions that are both called in the viewDidLoad(). At the end of each function is a bool that is set from false to true upon completion of the function. There is also a conditional statement checking both function's bools that fires a third function. This conditional statement is in both functions (that I want called when both of the two have finished). Generally:

var checkOne = false
var checkTwo = false

func functionOne(){
    //async stuff
    checkOne = true
    if checkOne == true && checkTwo == true{
        functionThree()//will only run if both functionOne and functionTwo have been completed
    }
}

func functionTwo(){
    //async stuff
    checkTwo = true
    if checkOne == true && checkTwo == true{
        functionThree()//will only run if both functionOne and functionTwo have been completed
    }
}

func functionThree(){
    //stuff
}


override func viewDidLoad() {

    functionOne()
    functionTwo()
}

This setup ensures that functionThree() can only be run when both functionOne and functionTwo are done. If functionOne finishes its async stuff before functionTwo() and gets to the conditional to fire functionThree(), it will not do it as checkTwo has not been made true yet. Thus, when functionTwo()'s async stuff is done, it will fire functionThree() . This works appropriately and has not caused an issue once. What I want to expressly avoid, though, is the async functions happening to finish, and therefore calling functionThree(), at the exact same time. To do this I would like to set an NSLock(), but, despite looking up documentation, I have zero clue how to do this as I need the same lock being handled by two different functions. Anyone have any ideas?

Galloromance answered 19/7, 2017 at 23:27 Comment(0)
R
44

An NSLock is a mutex; it prevents multiple threads from accessing the same resource simultaneously, which is exactly what you want to do here. Once one thread acquires the lock, other threads attempting to acquire the lock will wait until the first thread releases the lock.

You'll want to create a lock and store it somewhere that persists across and between function calls, most likely in an instance variable in this case. To acquire the lock, call its lock method, and to release it, use unlock:

var checkOne = false
var checkTwo = false

//create the lock
let lock = NSLock()

func functionOne(){
    //async stuff
    //acquire the lock
    lock.lock()
    checkOne = true
    if checkOne == true && checkTwo == true{
        functionThree()//will only run if both functionOne and functionTwo have been completed
    }
    //release the lock
    lock.unlock()
}

func functionTwo(){
    //async stuff
    lock.lock()
    checkTwo = true
    if checkOne == true && checkTwo == true{
        functionThree()//will only run if both functionOne and functionTwo have been completed
    }
    lock.unlock()
}

func functionThree(){
    //stuff
}


override func viewDidLoad() {

    functionOne()
    functionTwo()
}

A more "modern" approach is to use a DispatchQueue instead of an NSLock. Dispatch is higher-level than APIs like NSLock and NSThread; instead of directly working with locks and threads, you'll use queues.

A serial dispatch queue works like a checkout line at a store. You submit blocks of code to the queue, and it executes them one at a time in the order they were received. You can also create a concurrent dispatch queue which executes its tasks simultaneously by passing .concurrent to the options parameter of the DispatchQueue initializer.

A serial dispatch queue is an easy way to protect a resource from being accessed by multiple threads at once -- just create a queue for that resource, and put every access to that resource on the queue.

var checkOne = false
var checkTwo = false

//Create a serial dispatch queue
let queue = DispatchQueue(label: "name of queue")

func functionOne(){
    //async stuff

    //Add a task to the queue, and execute it synchronously (i.e. wait for it to finish.)
    //You can also use async to execute a task asynchronously,
    //but sync is slightly more efficient unless you need it to be asynchronous.
    queue.sync {
        checkOne = true
        if checkOne == true && checkTwo == true{
            functionThree()//will only run if both functionOne and functionTwo have been completed
        }
    }
}

func functionTwo(){
    //async stuff
    queue.sync {
        checkTwo = true
        if checkOne == true && checkTwo == true{
           functionThree()//will only run if both functionOne and functionTwo have been completed
        }
    }
}

func functionThree(){
    //stuff
}


override func viewDidLoad() {

    functionOne()
    functionTwo()
}
Rattish answered 19/7, 2017 at 23:37 Comment(3)
Great response, but I have a few questions. First about the NSLock method. It is surprisingly simple, but lets say functionOne() runs through enough such that it has called lock.lock() but not yet lock.unlock(). If functionTwo() then reaches its lock.lock(), it cannot proceed yet (right?). My question is, does functionTwo() basically just wait there and then proceed when it can? ie does it sit on the line lock.lock() until functionOne() unlocks the lock, and then functionTwo() proceeds? (Sorry I am just trying to understand exactly what's happening).Galloromance
As for the DispatchQueue method, what exactly is the queue.sync doing? Is only one code line allowed to run through queue.sync at a time, thus preventing both from ever firing at the same time? And if they do reach that point at the same time, what happens? Neither of them proceed, or is one selected at random, etc...?Galloromance
@AlekPiasecki Please see my edit. (If both threads reach the lock at the same time, exactly one of them is guaranteed to proceed; I do not believe there is any guarantee as to which one proceeds.)Rattish
A
8

Another approach using DispatchGroup. More simple, imho.

class ViewController: UIViewController {

    let group = DispatchGroup()

    override func viewDidLoad() {
        super.viewDidLoad()

        group.enter()
        functionOne()

        group.enter()
        functionTwo()

        group.notify(queue: .global(qos: .default), execute: { [weak self] in
            self?.functionThree()
        })
    }

    func functionOne() {
        //async stuff

        group.leave()
    }

    func functionTwo() {
        //async stuff

        group.leave()
    }

    func functionThree() {
        //stuff
    }
}
Amphetamine answered 26/11, 2018 at 12:33 Comment(1)
from my point (please correct me if I'm wrong) DispatchGroup waits for multiple sections of code (functions) to be completed. The Q was about locking a section so only one caller could enter the locked section, other should wait (be locked) in FIFOPolychaete
H
1

I think from Swift 5.7 you can also consider using actors to achieve thread-safe programming.

https://docs.swift.org/swift-book/documentation/the-swift-programming-language/concurrency/#Actors

Hebrides answered 27/9, 2023 at 14:9 Comment(1)
yes and this is probably the best way to do it nowadays, upvote for that. However in an existing codebase this might need way more refactoring than andding a queue only where necessary.Rinee

© 2022 - 2024 — McMap. All rights reserved.