Unexpected race condition with arrays

Viewed 24

I am trying to design a simple coffee machine system, that uses three resources which are: milk, coffee, water.

For each order I create a new thread, also for each thread I create another three threads one for each resource, in order to not wait for a free resource while accessing them serially.

According to the resources, I stored them in a simple array, and protect each index (which represent one resource of the three) with a lock, but unfortunately a race condition occurred when I turned on the TSAN.

I am pretty sure that no two threads will access the same index at the same time, and to be more confident I changed the resources from being inside an array to a seperate variables and no race occurred, so why when accessing the same array while protecting each index by a lock resulted in a race condition.

The following code is the one with resources array (the one that has race condition):

    import Foundation

    enum Resources: Int {
        case milk
        case coffe
        case water
    }

    struct Order {
        var id: Int
        var requiredMilk: Int
        var requiredCoffe: Int
        var requiredWater: Int
        
        init(requiredMilk: Int, requiredCoffe: Int, requiredWater: Int) {
            id = Int.random(in: 0...Int.max)
            self.requiredMilk = requiredMilk
            self.requiredCoffe = requiredCoffe
            self.requiredWater = requiredWater
        }
    }

    var resources: [Int] = []
    let dispatchGroup = DispatchGroup()
    let queue = DispatchQueue(label: "Ordering Queue", attributes: .concurrent)
    let locks = [NSLock(), NSLock(), NSLock()]

    func initiateResources(initialMilk: Int, initialCoffe: Int, initialWater: Int) {
        resources.append(contentsOf: [initialMilk, initialCoffe, initialWater])
    }

    func getAvailableMilk() -> Int {
        return resources[Resources.milk.rawValue]
    }

    func getAvailableCoffe() -> Int {
        return resources[Resources.coffe.rawValue]
    }

    func getAvailableWater() -> Int {
        return resources[Resources.water.rawValue]
    }

    func handleOrder(_ order: Order) {
        dispatchGroup.enter()
        // pushing the current order to the queue in order to be handled concurrenlty
        queue.async {
            let done = prepareOrder(order)
            if done {
                //print("Order with id \(order.id) prepared successfully")
            } else {
                //print("No sufficient resources for order with id \(order.id)")
            }
            dispatchGroup.leave()
        }
        
    }

    func prepareOrder(_ order: Order) -> Bool {
        var can = true
        let internalQueue = DispatchQueue(label: "internal queue", attributes: .concurrent)
        let internalDispatchGroup = DispatchGroup()
        let internalLock = NSLock()
        
        // opening a new thraed for each type of resources in order to not wait for a free resource
        if order.requiredMilk != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                locks[Resources.milk.rawValue].lock()
                if getAvailableMilk() < order.requiredMilk {
                    locks[Resources.milk.rawValue].unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 3)
                    resources[Resources.milk.rawValue] -= order.requiredMilk
                    locks[Resources.milk.rawValue].unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        
        if order.requiredCoffe != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                locks[Resources.coffe.rawValue].lock()
                if getAvailableCoffe() < order.requiredCoffe {
                    locks[Resources.coffe.rawValue].unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 2)
                    resources[Resources.coffe.rawValue] -= order.requiredCoffe
                    locks[Resources.coffe.rawValue].unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        
        if order.requiredWater != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                locks[Resources.water.rawValue].lock()
                if getAvailableWater() < order.requiredWater {
                    locks[Resources.water.rawValue].unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 1)
                    resources[Resources.water.rawValue] -= order.requiredWater
                    locks[Resources.water.rawValue].unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        // making sure that the three resources threads funished successfully
        internalDispatchGroup.wait()
        return can
    }

    func endWork() {
        dispatchGroup.wait()
    }

    // note that I commented the thread.sleep in order to run faster, but every resource should have different serving time

    initiateResources(initialMilk: 10000, initialCoffe: 10000, initialWater: 10000)
    for _ in 0..<10 {
        handleOrder(Order(requiredMilk: 5, requiredCoffe: 5, requiredWater: 5))
    }
    endWork()
    print(resources[0])
    print(resources[1])
    print(resources[2])

    /*
     running 90 threads ech need 5 units from each resource ==> total of 450 units
     So reaming resources should be 9550 from each type if no confclicts occured
     */

The following is the same one but whit separate variables for each resource (no race condition):

    import Foundation

    enum Resources: Int {
        case milk
        case coffe
        case water
    }

    struct Order {
        var id: Int
        var requiredMilk: Int
        var requiredCoffe: Int
        var requiredWater: Int
        
        init(requiredMilk: Int, requiredCoffe: Int, requiredWater: Int) {
            id = Int.random(in: 0...Int.max)
            self.requiredMilk = requiredMilk
            self.requiredCoffe = requiredCoffe
            self.requiredWater = requiredWater
        }
    }

    var milkRsource = 0, coffeResource = 0, waterResource = 0
    let dispatchGroup = DispatchGroup()
    let queue = DispatchQueue(label: "Ordering Queue", attributes: .concurrent)
    let milkLock = NSLock()
    let coffeLock = NSLock()
    let waterLock = NSLock()

    func initiateResources(initialMilk: Int, initialCoffe: Int, initialWater: Int) {
        milkRsource =  initialMilk
        coffeResource = initialCoffe
        waterResource = initialWater
    }

    func getAvailableMilk() -> Int {
        return milkRsource
    }

    func getAvailableCoffe() -> Int {
        return coffeResource
    }

    func getAvailableWater() -> Int {
        return waterResource
    }

    func handleOrder(_ order: Order) {
        dispatchGroup.enter()
        // pushing the current order to the queue in order to be handled concurrenlty
        queue.async {
            let done = prepareOrder(order)
            if done {
                //print("Order with id \(order.id) prepared successfully")
            } else {
                //print("No sufficient resources for order with id \(order.id)")
            }
            dispatchGroup.leave()
        }
        
    }

    func prepareOrder(_ order: Order) -> Bool {
        var can = true
        let internalQueue = DispatchQueue(label: "internal queue", attributes: .concurrent)
        let internalDispatchGroup = DispatchGroup()
        let internalLock = NSLock()
        
        // opening a new thraed for each type of resources in order to not wait for a free resource
        if order.requiredMilk != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                milkLock.lock()
                if getAvailableMilk() < order.requiredMilk {
                    milkLock.unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 3)
                    milkRsource -= order.requiredMilk
                    milkLock.unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        
        if order.requiredCoffe != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                coffeLock.lock()
                if getAvailableCoffe() < order.requiredCoffe {
                    coffeLock.unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 2)
                    coffeResource -= order.requiredCoffe
                    coffeLock.unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        
        if order.requiredWater != 0 {
            internalDispatchGroup.enter()
            internalQueue.async {
                waterLock.lock()
                if getAvailableWater() < order.requiredWater {
                    waterLock.unlock()
                    internalLock.lock()
                    can = false
                    internalLock.unlock()
                } else {
                    //Thread.sleep(forTimeInterval: 1)
                    waterResource -= order.requiredWater
                    waterLock.unlock()
                }
                internalDispatchGroup.leave()
            }
        }
        // making sure that the three resources threads funished successfully
        internalDispatchGroup.wait()
        return can
    }

    func endWork() {
        dispatchGroup.wait()
    }

    // note that I commented the thread.sleep in order to run faster, but every resource should have different serving time

    initiateResources(initialMilk: 10000, initialCoffe: 10000, initialWater: 10000)
    for _ in 0..<10 {
        handleOrder(Order(requiredMilk: 5, requiredCoffe: 5, requiredWater: 5))
    }
    endWork()
    print(milkRsource)
    print(coffeResource)
    print(waterResource)

    /*
     running 90 threads ech need 5 units from each resource ==> total of 450 units
     So reaming resources should be 9550 from each type if no confclicts occured
     */
0 Answers
Related