Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

This lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Hint: You can adjust the default video playback speed in your account settings.
Hint: You can set your subtitle preferences in your account settings.
Sorry! Looks like there’s an issue with video playback 🙁 This might be due to a temporary outage or because of a configuration issue with your browser. Please refer to our video player troubleshooting guide for assistance.

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:

Download

Sample Code (.zip)

12.9 KB

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 transactionwe 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.

fiy the wikipedia links to the Executor.submit python doc

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.

Become a Member to join the conversation.