iOS Concurrency in a Nutshell (Ep-IV)

Dispatch Barrier, Semaphore, Critical Section, Race Condition

Emin DENİZ
Orion Innovation techClub
11 min readMar 31, 2023

--

In the previous article ‘iOS Concurrency in a Nutshell’ we learned how we could manage multiple queues. In this article, we will focus on how we can preserve data consistency. You may remember famous threading problems like Deadlock or Race Conditions. To prevent these kinds of data inconsistency problems we might need to take some actions on GCD. Let’s start with a problematic example.

Example-1

Let’s have a simple UI that allows users to purchase tickets. UI will be as simple as in the image below.

Target UI for the Example-1

The first label at the top shows the user’s balance, and the other 2 labels show the ticket prices. The first 2 buttons start the purchase operation for each ticket. The ‘Buy All Tickets’ button purchases all the tickets. As you might imagine, we should start purchasing only if the user has enough balance. Here is the code for this example.

import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

/// Ep-4 Example-1
/// Dispatch Barrier
///

// 1. A concurrent to execute buy operations
let purchaseQueue = DispatchQueue(label: "com.example.buyQueue",
attributes: .concurrent)

// 2. View controller for the playground
class MyViewController : UIViewController {

// 3. Products sample array
let products: [Product] = [
Product(name: "Movie Ticket", price: 30),
Product(name: "Concert Ticket", price: 35)
]

// 4. Labels to show wallet and purchases.
let walletLabel = UILabel()
let purchaseHistoryLabel = UILabel()

var walletBalance = 50

override func loadView() {
// 5. createRootUIView function will initate all required UI elements
self.view = createRootUIView()
}

@objc func buyButtonTapped(_ sender: UIButton) {
// 6. Buy button action. (For single item)
purchaseQueue.async {
self.startPurchase(product:self.products[sender.tag])
}
}


@objc func allButtonClicked() {
// 7. Buy all items button action
for product in products {
// 8. Purchase all items one by one.
purchaseQueue.sync {
self.startPurchase(product: product)
}
}
}

func startPurchase(product: Product) {
print("Purchasing product: \(product), walletBalance: \(walletBalance)")

// 9. Prevent purchase if balance is not enough.
guard walletBalance >= product.price else {
print("Not enough balance to purchase \(product)")
DispatchQueue.main.async { [weak self] in
// 10. Update purchase history with warning.
self?.purchaseHistoryLabel.text = (self?.purchaseHistoryLabel.text ?? "")
+ "\nNot enough balance to buy \(product.name) ⚠️"
}
return
}

DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 11. Update balance and UI elements after purchase completed.
self.walletBalance -= product.price
self.walletLabel.text = "Wallet Balance: $\(self.walletBalance)"
self.purchaseHistoryLabel.text = (self.purchaseHistoryLabel.text ?? "") + "\n\(product.name) purchased ✅"
}
}
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

struct Product {
let name: String
let price: Int
}

Here are the key points for this code block that match my comments.

  1. We created a concurrent queue to execute tasks in a specific queue.
  2. Standard view controller for the playground.
  3. This array will hold all available products. For simplicity, I just add 2 products. The product is a struct that holds its name and price.
  4. Labels to show progress on the UI.
  5. There are a lot of elements in the UI for this example. I created a function called createRootUIView to prepare all the UI elements. It is not here for simplicity. But you can find it in the GitHub repo of this article series.
  6. Buy button action for each buy button. Depending on the button tag we will start purchasing the selected product.
  7. Buy all tickets button action.
  8. We will start purchasing each item one by one with the for loop. Realize that each iteration purchases the products by dispatching the operation to the asynchronous block.
  9. Prevent the user from purchasing any item if the balance is not enough.
  10. If the balance is insufficient, update the purchase history with a warning.
  11. Update the balance and UI.

In theory, this code should work. Let’s see the results if we try to buy the tickets individually.

Buying tickets one by one

As you can see it is working well. We can buy the Movie Ticket but the app won’t let us buy the Concert Ticket. Because after purchasing the Movie ticket we have 15$ which isn’t enough to buy a 35$ Concert Ticket. Let’s see what happens when we click the ‘Buy All Tickets’ button.

You should be realizing that we could buy both tickets even though we don’t have enough balance. Also, in the end, we have a -15$ balance, so it is nonsense for a user.

What is the problem here? We already prevent users to buy any more products at step 9. How the guard block couldn’t catch it?

This is the famous Race Condition problem. In step 8, we are calling the startPurchase function in a purchaseQueue block in an asynchronous manner. We have a guard block with the walletBalance variable but this value is updated in step 11 inside the main queue. In other words, we are checking the value of the walletBalance inside purchaseQueue but it is updated in the main queue. The dialog below visually demonstrates the problem. You can see that we are updating the walletBalance value in our code and in the diagram below.

Race condition demonstration.

You may think that ‘Ok I can update the walletBalance before dispatching the main queue and this will solve the problem’. You are totally correct if I update the value walletBalance in the same queue problem will be solved. But remember that, this is just an example. In reality, such problems usually can’t solve with these simple tricks. Let’s see the GCD ways to fix this problem.

Dispatch Barrier

Dispatch Barrier blocks the current dispatch queue until all previously submitted tasks finish executing. In our example, we are calling startPurchase function inside the asynchronous purchaseQueue (see step 8). So if we put a barrier at step 8 our problem will be solved. Let’s see the code for a particular position.

    @objc func allButtonClicked() {
// 7. Buy all items button action
for product in products {
// 8. Purchase all items one by one.
purchaseQueue.async(flags: .barrier) { [weak self] in
self?.startPurchase(product: product)
}
}
}

As you notice we are dispatching the task to queue with the barrier flag. As the name implies this will cause the purchase queue holds the upcoming tasks until the current one is completed. If that isn’t clear please see the diagram below.

Traffic lights in the diagram act as a barrier. When the ‘Movie Purchase Task’ starts the execution the barrier turns red and waits for the ‘Concert Purchase Task’. After the Purchase Queue cleared from the ‘Movie Purchase Task’ the barrier turns to green and execution of the ‘Concert Purchase Task’ begins. As you can see in the diagrams due to this synchronous approach ‘Concert Purchase Task’ won’t be successful. Here is the result.

This is might seem to you like using sync block but there is a slight difference. When we use sync blocks we are synchronously executing a particular task. In case you submit an async block alongside the sync block you can’t anticipate the order (See Example-4 in the first article). But when you use the barrier it won’t execute any task until the current one is completed. So regardless of sync and async blocks, you can be sure that your queue won’t execute any other task until the current execution is completed.

Example-2

In the previous example, we have only 2 queues, the purchase queue (for purchase) and the main queue (for UI update). In the startPurchase function we didn’t send any requests or do any actual time-consuming operations. Let’s push this example a little further.

import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

/// Ep-4 Example-2
/// Semaphores
///

// 1. Sample URL to demonstrate API call.
private let someRandomAPI = "https://run.mocky.io/v3/bf4523b3-1f46-4fa2-a376-5374aadea7e6?mocky-delay=2s"

class MyViewController : UIViewController {

let purchaseQueue = DispatchQueue(label: "com.example.buyQueue",
attributes: .concurrent)

let products: [Product] = [
Product(name: "Movie Ticket", price: 30),
Product(name: "Concert Ticket", price: 35)
]

let purchaseHistoryLabel = UILabel()
let walletLabel = UILabel()

var walletBalance = 50

override func loadView() {
self.view = createRootUIView()
}

@objc func buyButtonTapped(_ sender: UIButton) {
purchaseQueue.async {
self.startPurchase(product:self.products[sender.tag])
}

}

@objc func allButtonClicked() {
for product in products {
purchaseQueue.async(flags: .barrier) { [weak self] in
self?.startPurchase(product: product)
}
}
}

func startPurchase(product: Product) {
print("Purchasing product: \(product), walletBalance: \(walletBalance)")

guard walletBalance >= product.price else {
print("Not enough balance to purchase \(product)")
DispatchQueue.main.async { [weak self] in
self?.purchaseHistoryLabel.text = (self?.purchaseHistoryLabel.text ?? "")
+ "\nNot enough balance to buy \(product.name) ⚠️"
}
return
}

print("Starting purchase ⌛️")
// 3. Start actual API request
Network.shared.execute(urlString: someRandomAPI) { [weak self] result in

guard let self = self else {return}

switch result {
case .success(_):
print("Purchase success")
DispatchQueue.main.async {
self.walletBalance -= product.price
self.walletLabel.text = "Wallet Amount: $\(self.walletBalance)"
self.purchaseHistoryLabel.text = (self.purchaseHistoryLabel.text ?? "") + "\n\(product.name) purchased ✅"

}
case .failure(_):
print("Fast request fail ❌")
self.purchaseHistoryLabel.text = (self.purchaseHistoryLabel.text ?? "") + "\n\(product.name) purchase fail ❌"
}
}
}
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()

struct Product {
let name: String
let price: Int
}

This is very similar to the previous example except for the startPurchase function. Even though let’s see the key points for this code block that match my comments.

  1. We have a sample URL to demonstrate the API call.
  2. Same as in the previous example, we are dispatching the tasks with a dispatch barrier in an asynchronous manner.
  3. We are doing an actual API request to the mock API.
  4. In case the request is successful, we are decreasing the wallet amount in the main queue to update the UI.

Even though we don’t parse the mock response from the mock API this is a real-life example. If you are developing some shopping app you surely send some requests to API and depending on the result you are updating the UI. Let’s see the results.

What?… How?… Why?…

I can hear your reactions :) You will most probably say “We had a barrier it should hold the next task and this shouldn’t happen.”. But remember the definition we have for the Dispatch Barrier.

Dispatch Barrier blocks the current dispatch queue until all previously submitted tasks finish executing.

When we start the actual API request at step 3 we submit it to another queue with background QoS. So in this example, we have a total of 3 queues.

  • Purchase queue to execute purchase tasks.
  • The main queue to update the UI
  • A queue to execute API requests with background QoS.

Even though we don’t have a separate queue in the Network class, URLSession dispatches the task to a queue with background QoS.

So the dispatch barrier can’t solve the second example. What can be the answer?

Semaphore

You can hear semaphores in almost every programming language. Even though the name can be scary at first idea of the semaphore is very easy. Semaphores have a counter value that we define when we first create them. When the critical section (startPurchasefunction) starts, we manually decrement the counter of semaphore. When the counter is zero, the semaphore won’t allow any other tasks to be executed from any dispatch queue. After the critical section execution is completed, we manually increment the counter of the semaphore. This will allow the semaphore to execute the next waiting task. And that's it! This is how semaphores work.

Demonstration of how semaphore works

As you can see in the demonstration above, the working mechanism of semaphore is very similar to dispatch barriers. The difference is we don’t need to manually call a GCD API to wait or execute a new task in dispatch barriers. It automatically executes a new task if the current execution is completed. But semaphore needs us to call Wait and Signal functions in critical sections.

Let’s see it in action.

import UIKit
import PlaygroundSupport
PlaygroundPage.current.needsIndefiniteExecution = true

/// Ep-4 Example-2
/// Semaphores
///

// 1. Sample URL to demonstrate API call.
private let someRandomAPI = "https://run.mocky.io/v3/bf4523b3-1f46-4fa2-a376-5374aadea7e6?mocky-delay=2s"

class MyViewController : UIViewController {

let purchaseQueue = DispatchQueue(label: "com.example.buyQueue",
attributes: .concurrent)
// 1. Sempahore with initial value (counter) 1
let semaphore = DispatchSemaphore(value: 1)

let products: [Product] = [
Product(name: "Movie Ticket", price: 30),
Product(name: "Concert Ticket", price: 35)
]

let purchaseHistoryLabel = UILabel()
let walletLabel = UILabel()

var walletBalance = 50

override func loadView() {
self.view = createRootUIView()
}

@objc func buyButtonTapped(_ sender: UIButton) {
purchaseQueue.async {
self.startPurchase(product:self.products[sender.tag])
}

}

@objc func allButtonClicked() {
for product in products {
purchaseQueue.async(flags: .barrier) { [weak self] in
self?.startPurchase(product: product)
}
}
}

func startPurchase(product: Product) {
print("Purchasing product: \(product), walletBalance: \(walletBalance)")

// 2. Critical section begins, wait semaphore
semaphore.wait()

guard walletBalance >= product.price else {
print("Not enough balance to purchase \(product)")
DispatchQueue.main.async { [weak self] in
self?.purchaseHistoryLabel.text = (self?.purchaseHistoryLabel.text ?? "")
+ "\nNot enough balance to buy \(product.name) ⚠️"
}
return
}

print("Starting purchase ⌛️")
// 3. Start actual API request
Network.shared.execute(urlString: someRandomAPI) { [weak self] result in

guard let self = self else {return}

switch result {
case .success(_):
print("Purchase success")
DispatchQueue.main.async {
self.walletBalance -= product.price
// 4. Crtical section ends, signal semaphore
self.semaphore.signal()
self.walletLabel.text = "Wallet Amount: $\(self.walletBalance)"
self.purchaseHistoryLabel.text = (self.purchaseHistoryLabel.text ?? "") + "\n\(product.name) purchased ✅"

}
case .failure(_):
print("Fast request fail ❌")
self.purchaseHistoryLabel.text = (self.purchaseHistoryLabel.text ?? "") + "\n\(product.name) purchase fail ❌"
// 5. Crtical section ends, signal semaphore
self.semaphore.signal()
}
}
}
}

// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()


struct Product {
let name: String
let price: Int
}

Here are the key points for this code block that match my comments.

  1. We initialized a semaphore with an initial value (counter) of 1. This means our semaphore won’t allow the execution of more than 1 task at a time.
  2. The critical section in our code starts when we check the wallet amount. That’s why we called semaphore.wait() in here. Wait will decrease the counter of semaphore.
  3. Sending actual API request.
  4. After updating walletBalanance is updated the critical section ends. We can call the signal function here to increment the counter. This will allow the semaphore to execute waiting tasks.
  5. Also, signal the semaphore for failed cases.

Let’s see the results.

As you can see our application working correctly now. 🎉

Summary

In this article of the series, we discussed important topics such as Dispatch Barriers and Semaphores. In each article, you and I learn a lot of things about concurrency and hopefully master it.

I encourage you to play with the topics we talked about in this article. Here is the link to the repository that contains examples in this article.

Take care till we meet again!

Articles in the series

--

--