Solving Races With Locks
00:00 In the previous lesson, I introduced you to our banking problem and how a race condition means the wrong balance. In this lesson, I’ll show you one solution, adding atomicity in your code through locks.
00:12 The way to create atomicity in programs is to have a locking mechanism. The Global Interpreter Lock is just such a device. The GIL is a CPython implementation detail that ensures thread safety around memory management routines.
00:26 It’s a bit of a compromise. You don’t want race conditions on memory management, otherwise you’ll leak or overwrite memory, which is bad. But the GIL isn’t such an aggressive lock that it gets rid of all race conditions, just those at the low level.
00:40 Like with all locks, when the GIL is engaged, all threads have to wait until it’s unlocked again. This limits the amount of concurrency you can do with threads in Python.
00:50 Whether your code needs a lock or not, under it all, there is one big lock that can get in the way. This is why it’s recommended to only use threading for I/O bound code.
01:00 If your code is CPU bound, the GIL is likely to remove any sort of parallelism you thought you would have. And if all that isn’t complicated enough, remember a second ago when I said the GIL was a CPython implementation detail?
01:12 Well, one consequence of that is the implementation detail can change. In fact, some sample code I wrote in another course to show off a race condition all of a sudden stopped having a race condition in it.
01:24 That happened because of changes in the interpreter about when the GIL was locked or not. So in one version of Python, the race condition is there, and in another version of Python, the GIL was accidentally taking care of it for me.
01:36 Of course, you shouldn’t be dependent on this at all. Future changes to Python may cause the assumption you made about the GIL to be wrong, and that’s without getting into the ongoing work to get rid of the GIL altogether.
01:49 So you can’t trust the GIL to solve the race condition problem for you. And even if you currently don’t have a race condition because the GIL is taking care of it, there’s a chance that won’t be true in all versions of Python.
02:01 Instead, you should use your own locks, putting them in the part of your code that needs protection. This way, you are in full control of when your code is atomic or not.
02:11
The first lock I’m going to introduce you to is the Lock
class in the threading
library, it has two key methods. The first, .acquire()
, is a blocking call.
02:21 Your code stops here until it obtains the lock. If your code has the lock, all others attempting to acquire it will be blocked until you’re done.
02:30
And the second method is .release()
, which is how you release a previously acquired lock for future use. The essence of this is only one thread can acquire a given lock at a time.
02:42
Instead of using .acquire()
and .release()
, I recommend using the lock as a context manager instead. The controlled block won’t be entered until the lock has been obtained and will automatically be released on exit of the block.
02:54 Using a context manager is the best way to use locks, as it means you won’t accidentally forget to release a lock and create a situation where your code will block forever.
03:04 Let’s revisit our banking service fee problem, this time with a lock to make our account changes atomic.
03:11
You’ll need the threading
library as that’s where the lock object lives, and this is how you construct the lock object. For our situation, each instance of an account object gets its own lock, and that works fine.
03:24 Account one doesn’t have to wait if account two is being updated. This works because the different accounts don’t share any information.
03:33 Here, I’ve moved all of our balance changing code inside of a context block based on the lock. This block of code won’t run until the lock has been acquired and the lock will get released once the block exits.
03:47 Let me scroll down a little.
03:50 Don’t forget, you have to make all the code that uses a shared resource atomic. So I need to lock in both the withdrawal and the deposit code as well. Inside the deposit code, I have the same idea as I do in withdrawal.
04:04 Now that I’ve got a lock surrounding the two blocks where the balance gets updated, you won’t be able to complete a withdrawal and deposit at the same time.
04:12 The code in each of the methods is guarded by the lock.
04:17 Oh, one small thing I kind of skipped over. Since using a delay of half a second consistently caused our race condition, I have hard-coded it this time around so there’s no arguments anymore.
04:28 Alright, let’s try this code out. Running the script.
04:38 And there you go. All clean. The race condition has gone away.
04:42 The lock is the core primitive of mutual exclusion in programming. In fact, every other synchronization mechanism from here on is built on top of a lock. To save you having to code more complex mechanisms yourself though, Python provides a bunch of other choices.
04:58 In the next lesson, I’ll show you a variation on what you’ve just seen called the reentrant lock.
Become a Member to join the conversation.