Race conditions, threads, processes, tasks, queues in iOS

What is a race condition?

When you have two threads changing a variable simultaneously. It’s possible to get unexpected results. Imagine a bank account where one thread is subtracting a value to the total and the other is adding a value.

The order of events goes like this:

  1. Access the total to subtract to it. (total = 500, expense = 40)
  2. Compute the difference. (total =500, newTotal = 460)
  3. Save the new total. (total = 460)
  4. Access the total to add to it. (total = 460, income = 200)
  5. Compute the sum (total = 460, newTotal = 660)
  6. Save the new total (total = 660)

Great! 660 is exactly what we expected, but it could have gone like this:

  1. Access the total to subtract to it. (total = 500, expense = 40)
  2. Access the total to add to it. (total = 500, income = 200)
  3. Compute the difference. (total =500, newTotal = 460)
  4. Save the new total. (total = 460)
  5. Compute the sum (total = 500, newTotal = 700)
  6. Save the new total. (total = 700)

So the account is now at 700. That’s not what we expected. Uh oh.

When should we suspect a race condition?

When variables that have multiple threads operating on them and you’re getting unexpected results some of the time.

How do we fix this?

One option is to lock the variable total so that only one thread could operate on it at a time, the lock is called a mutex.

But in iOS, you want to use grand central dispatch (GCD) or OperationQueues to handle your tasks. So in the example of the bank total, we’d probably just want to use a serial dispatch queue which has sole access to the total variable. By using GCD or OperationQueues, you’re letting Apple do the thread management and removing the code for locking. Less code is good.

So what are threads, processes and tasks?

The task is an abstract concept for describing something you want to do. In the example above, adding money to the total or subtracting money from the total are both tasks. They use a process to actually affect the change in code.

A process keeps track of what needs to be done and delegates the tasks to the threads. A process can have one or multiple threads. The process is like a project manager and the thread is like a worker.

What’s the difference between hardware and software threads?

The threads we’ve been talking about so far have been software threads. They’re (generally) independent units of computation.

The hardware threads are based on the number of cores on the computer. For example, the current 12 inch MacBook has this processor with a 1.1GHz two-core processor. That means it have 2 cores, each with 1.1GHz of clock speed. Each of those core have two 2 hardware threads, so there are 4 hardware threads total.

Each of these hardware threads can run many software threads, depending on how the operating system uses them.

Playing with iOS MapKit Part 4: Race Conditions, Other Bugs, Wrap-up

Playing With MapKit

Part 1 MapKit Tutorials 

Part 2 Reverse Geocoding and Custom Annotation Callouts

Part 3 Making it Pretty

Part 4 Race Conditions, Other Bugs, and Wrap-up

As the functionality and the look was coming into place, I took note of bugs that I saw.

The most insidious bugs are ones that don’t happen all the time or under conditions (ex. when internet is especially slow or the computer is especially fast). One example is the race condition.

What is a Race Condition?

A race condition is a situation when the sequence of asynchronous responses results in an undesired effect.

My Example

Very quickly after starting the app, I was tapping the current location dot and I was seeing “Current Location” in the bubble. It should have said the address that the address was closest to. If I waited a couple seconds to tap the current location, it would display the address as expected.

This led me to believe that I had a race condition.

Under normal conditions:

  1. The map updates the user location calling its delegate mapView:didUpdateUserLocation: which starts the reverseGeocoding call based on the user’s location.
  2. The reverseGeocoding response is received and the completion block sets the annotation’s title with the address of the user location.
  3. When the user location dot is tapped, it will display a custom bubble using the annotation’s title in the label.

Usually the internet is fast enough that the reverseGeocoding response returns before the user location dot is tapped.

The two things that are racing are

  1. the reverseGeocoding network call and
  2. the tap of the current location dot.

If 1 finished first, then the app would work as intended. If 2 finished first, the experience would be suboptimal.

Design Choice

One way to fix this is to update the label when the reverseGeocoding network call came back.

At the end of the completion block, I added a method call to updatePinCallout, which pretends that the user location dot was tapped again and reloads the address.

- (void)updatePinCallout
{    
  if (self.userLocationPin.selected) {
   [[NSOperationQueue mainQueue] addOperationWithBlock:^{
    [self.userLocationPin setSelected:YES animated:YES];
   }];
  }
}

Just remember to use the mainQueue to retap the dot or else the address won’t update on screen.

This change also fixed another bug that happened when the user location would jump when WIFI was enabled and the address label would not update to the new address.

Alternative Choice

Another thing that I found solved the problem pretty well was to zoom the map to the user location immediately, so that the animation didn’t even give a chance to tap the user location dot.

Other Bugs

Writing code for multiple scenarios unnecessarily

When I first implemented zooming to fit destinations, I only zoomed when the location was outside of the visible region. I realized that sometimes this would not be sufficient because sometimes the map would be zoomed way out, so I decided to account for that scenario too. I would zoom in if the destination was in the original visible area. This got complicated quickly.

Eventually, I realized that it was too much and just made it always zoom to the destination. The code went from 14 lines to 1.

Simpler is Better

Testing Days Later

I thought I was done with the project. Wrong. A few days later, I was trying out the app on the bus and came across two bugs.

  1. I was on a bus and the location was updating very quickly and constantly centering the map on the new location. The problem was that I wanted to look around the map and couldn’t do that without it changing while I was using it.
    • This is easily fixed by only zooming on the time the map loads.
  2. I was looking for a Pret A Manger. When I saw the results page, I found it utterly useless because all of them said the same thing and I couldn’t tell which location was where.
    • I think it would have been cool to add some directions as to how far each location was from my current location and in which direction or add the cross streets.

As the Pragmatic Programmer said:

“Test your software, or your users will.”

Where to next?

There’s certainly a lot of ways this can go. Add friends, add Facebook integration, add Yelp results, use Google Maps. That would be fun to do, but would lack direction.

For a product to be real, it must have a real use.

The way that this app fits into my life has something to do with lunch (ergo the name LunchTime). In an effort to build good habits, one of them is to walk 10K steps a day. As I’ve discovered, this means walking 1.3 hours a day. If I don’t plan to walk, I won’t get near 10K.

So over lunch, I like to walk 10 to 15 minutes away and back. I think it would be cool to draw a diamond around your current location to see how far you can walk in 5 and 15 minutes and show how far each location is.

Options abound. Looking forward to the next expedition.

It’s been fun working with MapKit. Time to play around with something else. Who knows? I might be looking at WatchKit, EventKit, or something else entirely.