Example: Bank Account Program
In this lesson, you’ll create a bank account program that demonstrates a race condition. If you download the sample code, you can get your own copy of 09-account.py
:
To learn more, you can also check out the documentation for concurrent.futures.Executor.submit
as well as the software section of the Wikipedia page on race conditions.
00:00 In the previous lesson, you learned what a race condition is, which is when more than one thread tries to access a piece of shared data at the same time. Now you’ll write a program that demonstrates that.
00:12
Import the concurrent.futures
module as well as the time
package. Then create a class called Account
, and this will represent a bank account.
00:25
Give it an .__init__()
method and a starting .balance
.
00:32
I’m going to start it at 100
, just like the previous slide showed the starting balance of $100. And then give your Account
a method to change the .balance
.
00:42
Let’s call it .update()
. It takes a transaction
, it’s going to be either 'withdrawal'
or 'deposit'
, and an amount
for that transaction. In a real-world situation, we would be reading this self.balance
from some type of database.
00:59
And just to be clear, this is the shared data that our threads are going to try to read and write. In the .update()
method, make a local_copy
of self.balance
as a way to kind of replicate reading the current balance from a database. So when our deposit thread, for example, drops into this method, it’s going to read the balance from the database and store it as a local_copy
.
01:29
And then take your local_copy
and either add or subtract the amount
that you’re going to pass to this method.
01:38
Throw in a time.sleep()
to represent the time it takes to read the .balance
and manipulate it, and I’m just going to do 1
second this time.
01:49
And then we’ll write the new balance, aka the local_copy
, to the database by just reassigning self.balance
to our local_copy
. So outside of your class, create an entry point.
02:04
if __name__ == '__main__':
and then create an Account
. So, account = Account()
. And then add a print statement—let’s say f'starting with balance of {account.balance}'
.
02:23
And then we want to create two threads: a deposit thread, and a withdrawal thread. We’re going to use our ThreadPoolExecutor
, so with concurrent.futures.ThreadPoolExecutor()
and recall that we need to pass in our max_workers
parameter here, and we’re going to use two threads, so let’s make it 2
, and let’s call it ex
.
02:53
Our executor is going to start and join the threads using a function called .submit()
. We’ll do ex.submit()
and this is going to take a target function, so our target function is going to be the account.update
, and then we are going to pass in the arguments. So our transaction
—we want this to be either 'deposit'
or 'withdrawal'
and then the amount
that we want to act upon that.
03:21
Right above the .submit()
call, create a loop. We’ll do for transaction, amount
in a list of tuples,
03:32
and the first element of the tuple is going to be our transaction and the second will be the amount. Let’s first do a 'deposit'
03:43
of 150
. And then in the next thread, represented by this tuple, we’ll do a 'withdrawal'
.
03:52
And the amount we’re going to withdraw is 150
, but since we’re doing a +=
on our local_copy
, we actually would want to write a -150
.
04:03
I accidentally wrote 150
—I meant to write 50
. So, I’m trying to replicate what we had in the slides here with a .balance
of a 100
and then a deposit of 50
, which should make our .balance
150
, and then we withdraw -150
, so that should leave us with an ending .balance
of 0
.
04:22
But let’s finish our .submit()
call here, so first let’s indent that so we’re doing it within the loop. And then for our arguments, we don’t have to pass them as an iterable for .submit()
; we can pass them as positional arguments.
04:35
We can just say transaction, amount
and that’s going to create two threads—starting them and joining them. Once we exit our loop and our threads finish executing, we’ll jump back to the main thread and let’s print the f'ending balance of {account.balance}'
.
04:58 Before we go and execute our program, let’s add in a couple more print statements so you can really see, step by step, what is happening with each thread.
05:08
At the beginning of the .update()
method enter a print statement. We’ll say f'{transaction} thread updating...'
,
05:20
and then at the end, we’ll just say f'finishing...'
.
05:29
And then I’m going to go ahead and open up a terminal. I named this file 09-account.py
, so let’s go ahead and execute that.
05:44
All right, we have an invalid syntax
here, so this looks like I forgot a colon at the end of my for
loop. Let’s clear the screen and try that again. All right, we have a starting with balance of 100
.
05:58
That’s coming from our main thread. And we fire off the deposit thread with an amount
of 50
, so we see deposit thread updating...
. That means it’s in this .update()
method right now. It’s made a local_copy
of the .balance
, which should be 100
.
06:13
And then it added 50
to it and it went to sleep. When it went to sleep, it switched context to the withdrawal thread, which started, and that’s why we see withdrawal thread updating...
here. The withdrawal thread is reading from self.balance
. Now, here’s the tricky part.
06:30
The deposit thread is here. It’s sleeping. That means it hasn’t reached this line here—it hasn’t actually set the self.balance
to 150
.
06:42
The self.balance
is still 100
. The withdrawal thread is reading in 100
as its local_copy
and then the amount
for the withdrawal thread was -150
.
06:55
So, 100
plus -150
is -50
. So when the deposit thread wakes up, then it resets self.balance
and then it exits.
07:08
The withdrawal thread went to sleep—remember that local_copy
is actually -150
now—and then when it wakes up, it sets self.balance
to -50
.
07:20
It’s 100
minus -150
, so that’s why we see the ending balance of -50
. There’s a crucial step where the withdrawal thread actually didn’t get the shared data that we wanted it to, because the deposit thread was still asleep.
07:41
This is an example of a race condition, and you just created a program in Python that represents that using this ThreadPoolExecutor
and your knowledge of how threads work.
07:51
As we move forward, we’re going to learn about locks and mutexes—a component of the threading
package that can prevent behavior like this.
malbert137 on June 1, 2020
Presumably at the exit of the context manager, the thread pool joins all of the “submitted” threads?
dlasusa0 on Oct. 7, 2020
Win10 Python 3.8.5
Not sure if anything changed, but running the 09-account.py file from the download zip (and it looks the same as the ending code in the video), I get a different result:
starting with balance of 100
deposit thread updating...
withdrawal thread updating...
withdrawal thread finishing...
deposit thread finishing...
ending balance of 150
I can see why it’s 150 (the deposit thread is finishing last and therefore setting the account.balance to 150, but I’m not sure why this is happening.
The code as it’s being run:
import concurrent.futures
import time
class Account:
def __init__(self):
self.balance = 100 # shared data
def update(self, transaction, amount):
print(f'{transaction} thread updating...')
local_copy = self.balance
local_copy += amount
time.sleep(2)
self.balance = local_copy
print(f'{transaction} thread finishing...')
if __name__ == '__main__':
account = Account()
print(f'starting with balance of {account.balance}')
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as ex:
for transaction, amount in [('deposit', 50), ('withdrawal', -150)]:
ex.submit(account.update, transaction, amount)
print(f'ending balance of {account.balance}')
dlasusa0 on Oct. 7, 2020
Nevermind. I found by adjusting the sleep time (I tried both 2 and 5) and running the code multiple times it would sometimes be 150 and sometimes -50. So I’m guessing the issue is scheduling with a busy cpu?
jeanlouisgosselin123 on Dec. 29, 2020
@dlasusa
I have experienced the same thing as you. It may very well be that the issue is scheduling with a busy CPU. This makes me question however the pertinence of implementing a timer with time.sleep()
… If a busy CPU overrides this timer, what is the point of including this timer in the program?
Andrew Jarcho on Feb. 17, 2021
@LeeGaines, you’ve developed an excellent course so far. But this lesson (running 09-account.py) needs improvement.
You say that a race condition has been created, which is true. But the result of a race condition is that different runs of the same code can produce different output. In this example, running 09-account.py can print an ending balance of either 150 or -50.
The lesson should show runs that produce both outputs.
Since you don’t show both outputs, your students don’t get to see both possible outcomes of the race condition occur. So they can be confused, as the above comments show.
Shubham Prasad on Aug. 19, 2024
hi all, i think some explaination regarding .map and submit function is important. one video on different use cases of all methods of ThreaPoolExecutor will be good.
Become a Member to join the conversation.
RMS on April 28, 2020
fiy the wikipedia links to the Executor.submit python doc