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
*/