├── .gitignore ├── 01-single_thread.py ├── 02-multi_threaded.py ├── 03-daemon_threads.py ├── 03-daemon_threads_timeout.py ├── 03-daemon_threads_v2.py ├── 04-join_threads.py ├── 04-join_threads_timeout.py ├── 04-join_threads_v2.py ├── 05-thread_pool.py ├── 05-thread_pool_futures.py ├── 06-race_condition.py ├── 06-race_condition_with_lock.py ├── 07-lock.py ├── 08-deadlock.py ├── Dockerfile ├── GIL.md ├── Makefile ├── README.md ├── example_file_processor.py ├── example_multiprocessing.py ├── example_spinner_thread.py ├── example_webscraper.py ├── requirements.txt ├── training.py └── tree.png /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | logs 3 | -------------------------------------------------------------------------------- /01-single_thread.py: -------------------------------------------------------------------------------- 1 | # Example of a single-threaded application 2 | from training import WEBSITES, visit_website 3 | 4 | 5 | if __name__ == '__main__': 6 | print('Main thread starting') 7 | for website in WEBSITES: 8 | visit_website(website) 9 | print('Main thread ending') -------------------------------------------------------------------------------- /02-multi_threaded.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application 2 | from training import WEBSITES, visit_website 3 | import threading 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | for website in WEBSITES: 9 | # Create a Thread object with target and args 10 | t = threading.Thread(target=visit_website, args=[website]) 11 | # Start the thread 12 | t.start() 13 | print('Main thread ending') -------------------------------------------------------------------------------- /03-daemon_threads.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using daemon threads 2 | from training import WEBSITES, visit_website 3 | import threading 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | for website in WEBSITES: 9 | # Create a Thread object with target and args 10 | t = threading.Thread(target=visit_website, args=[website], daemon=True) 11 | # Start the thread 12 | t.start() 13 | print('Main thread ending') -------------------------------------------------------------------------------- /03-daemon_threads_timeout.py: -------------------------------------------------------------------------------- 1 | # Example of using daemon threads to terminate threads when the main thread terminates 2 | from training import WEBSITES, visit_website 3 | import threading 4 | import sys 5 | import time 6 | 7 | 8 | if __name__ == '__main__': 9 | # Create a forced timeout option 10 | if len(sys.argv) != 2: 11 | print(f'Usage: {sys.argv[0]} TIMEOUT') 12 | sys.exit() 13 | 14 | print('Main thread starting') 15 | 16 | # The program will end after `timeout` seconds 17 | timeout = int(sys.argv[1]) 18 | 19 | for website in WEBSITES: 20 | # Create and start some daemon threads 21 | t = threading.Thread(target=visit_website, args=[website], daemon=True) 22 | t.start() 23 | 24 | # Force the program to end after timeout 25 | time.sleep(timeout) 26 | 27 | print('Main thread ending') -------------------------------------------------------------------------------- /03-daemon_threads_v2.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using daemon threads 2 | from training import WEBSITES, log_website 3 | import threading 4 | import time 5 | 6 | 7 | if __name__ == '__main__': 8 | print('Main thread starting') 9 | for website in WEBSITES: 10 | # Create a Thread object with target and args 11 | t = threading.Thread(target=log_website, args=[website], daemon=True) 12 | # Start the thread 13 | t.start() 14 | 15 | # Mock some time spent doing other things 16 | time.sleep(1) 17 | 18 | # Ok, now we are done 19 | print('Main thread ending') -------------------------------------------------------------------------------- /04-join_threads.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using start() and join() 2 | from training import WEBSITES, visit_website 3 | import threading 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | for website in WEBSITES: 9 | # Create, start, and join a thread 10 | t = threading.Thread(target=visit_website, args=[website]) 11 | t.start() 12 | t.join() 13 | print('Main thread ending') -------------------------------------------------------------------------------- /04-join_threads_timeout.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using start() and join() with a timeout 2 | from training import WEBSITES, visit_website 3 | import threading 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | for website in WEBSITES: 9 | # Create, start and join a daemon thread that times out 10 | t = threading.Thread(target=visit_website, args=[website], daemon=True) 11 | t.start() 12 | t.join(timeout=1) 13 | print('Main thread ending') -------------------------------------------------------------------------------- /04-join_threads_v2.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using start() and join() 2 | import threading 3 | 4 | chunks = ['abcdefghij', 'klmnopqrs', 'tuvwxyz\n'] 5 | threads = [] 6 | 7 | def writer(chunk): 8 | with open('alphabet.txt', 'a') as f: 9 | f.write(chunk) 10 | 11 | if __name__ == '__main__': 12 | print('Main thread starting') 13 | 14 | for chunk in chunks: 15 | # Create and start all threads (in order) 16 | t = threading.Thread(target=writer, args=[chunk]) 17 | threads.append(t) 18 | t.start() 19 | 20 | # Wait for all threads to finish 21 | for thread in threads: 22 | thread.join() 23 | 24 | with open('alphabet.txt') as f: 25 | print(f.read()) 26 | 27 | print('Main thread ending') -------------------------------------------------------------------------------- /05-thread_pool.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using ThreadPoolExecutor 2 | from training import WEBSITES, visit_website 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | # Use the ThreadPoolExecutor context manager to manage threads 9 | with ThreadPoolExecutor(max_workers=3) as executor: 10 | for website in WEBSITES: 11 | # Submit a function and args to the pool of threads 12 | executor.submit(visit_website, website) 13 | print('Main thread ending') -------------------------------------------------------------------------------- /05-thread_pool_futures.py: -------------------------------------------------------------------------------- 1 | # Example of a multi-threaded application using ThreadPoolExecutor 2 | from training import WEBSITES, visit_website 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | if __name__ == '__main__': 6 | print('Main thread starting') 7 | 8 | # Collection of future objects 9 | futures = [] 10 | 11 | with ThreadPoolExecutor(max_workers=20) as executor: 12 | for website in WEBSITES: 13 | # Submit a function and args to the pool of threads 14 | futures.append(executor.submit(visit_website, website)) 15 | 16 | # Iterate over the results 17 | for future in futures: 18 | print(future.result()) 19 | 20 | print('Main thread ending') -------------------------------------------------------------------------------- /06-race_condition.py: -------------------------------------------------------------------------------- 1 | # Example of a race condition 2 | from training import Account 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | account = Account() 9 | print(account) 10 | with ThreadPoolExecutor(max_workers=2) as executor: 11 | # Submit two threads: deposit 100, followed by withdrawal 50 12 | for transaction, amount in [(account.deposit, 100), (account.withdrawal, 50)]: 13 | executor.submit(transaction, amount) 14 | print(account) 15 | print('Main thread ending') 16 | 17 | -------------------------------------------------------------------------------- /06-race_condition_with_lock.py: -------------------------------------------------------------------------------- 1 | # Example of a race condition 2 | from training import ThreadSafeAccount 3 | from concurrent.futures import ThreadPoolExecutor 4 | 5 | 6 | if __name__ == '__main__': 7 | print('Main thread starting') 8 | account = ThreadSafeAccount() 9 | print(account) 10 | with ThreadPoolExecutor(max_workers=2) as executor: 11 | # Submit two threads: deposit 100, followed by withdrawal 50 12 | for transaction, amount in [(account.deposit, 100), (account.withdrawal, 50)]: 13 | executor.submit(transaction, amount) 14 | print(account) 15 | print('Main thread ending') 16 | 17 | -------------------------------------------------------------------------------- /07-lock.py: -------------------------------------------------------------------------------- 1 | # Example of a locking using Lock objects 2 | import threading 3 | 4 | 5 | if __name__ == '__main__': 6 | 7 | # Create a lock 8 | lock = threading.Lock() 9 | print(lock) 10 | 11 | # Acquire a lock for a section of code 12 | lock.acquire() 13 | print(lock) 14 | 15 | # Release the lock 16 | lock.release() 17 | print(lock) 18 | 19 | """ 20 | 21 | lock = threading.Lock() 22 | 23 | SHARED_DATA = 1 24 | 25 | def func(): 26 | 27 | lock.acquire() 28 | SHARED_DATA += 1 29 | lock.release() 30 | 31 | """ 32 | -------------------------------------------------------------------------------- /08-deadlock.py: -------------------------------------------------------------------------------- 1 | # Example of a deadlock situation. 2 | # Threads within a process share the same memory space. 3 | import threading 4 | 5 | 6 | if __name__ == '__main__': 7 | 8 | # Create a lock 9 | lock = threading.Lock() 10 | print(lock) 11 | 12 | # Acquire a lock 13 | lock.acquire() 14 | print(lock) 15 | 16 | # Acquire a lock again -- deadlock! 17 | lock.acquire() 18 | print(lock) 19 | 20 | # Release the lock 21 | lock.release() 22 | print(lock) 23 | 24 | """ 25 | 26 | lock = threading.Lock() 27 | 28 | SHARED_DATA = 1 29 | 30 | def func(): 31 | 32 | lock.acquire() <-- another thread tries to access SHARED_DATA 33 | lock.acquire() 34 | SHARED_DATA += 1 35 | lock.release() 36 | 37 | """ 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12 2 | WORKDIR /src 3 | RUN apt-get update && apt-get install -y man htop imagemagick 4 | ADD ./requirements.txt /src/requirements.txt 5 | RUN pip install -r requirements.txt 6 | ADD . /src -------------------------------------------------------------------------------- /GIL.md: -------------------------------------------------------------------------------- 1 | # GIL (Global Interpreter Lock) 2 | 3 | Python has _real_ threads, but only one thread can hold the GIL at a time, so only one thread gets executed at a time. 4 | 5 | `sys.getswitchinterval()` shows how often the Python interpreter pauses the current thread (how often the GIL gets released). 6 | 7 | > Every Python standard library function that makes a syscall releases the GIL. This includes all functions that perform disk I/O, network I/O, and time.sleep() 8 | 9 | > The effect of the GIL on network programming with Python threads is relatively small, because the I/O functions release the GIL, and reading or writing to the network always implies high latency—compared to reading and writing to memory. 10 | 11 | ## Why does it exist? 12 | To simplify memory management. Without the GIL, developers would have to implement more fine-grained locking mechanisms to prevent multiple threads from simultaneously modifying Python objects, which could lead to data corruption and other concurrency issues. 13 | 14 | ## PEP703 15 | Making the Global Interpreter Lock Optional in CPython ([link](https://peps.python.org/pep-0703/)) 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | build: 2 | docker build -t py3-threads . 3 | run: build 4 | docker run -it --mount type=bind,source=$$(pwd),target=/src py3-threads bash 5 | clean: 6 | @find . -name '__pycache__' | xargs rm -rf; 7 | @find . -name '*.log' | xargs rm -rf; 8 | @find . -name 'alphabet.txt' | xargs rm -rf; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Build Fast and Efficient Python Applications Using Threads 2 | 3 | Source code for the O'Reilly live online training with Lee Gaines. 4 | 5 | We will use a Python3 Docker image to run the code. Assuming you have [installed Docker](https://docs.docker.com/get-docker/), navigate to the `src/threads` directory of this repo, then build the image with the following command: 6 | 7 | ``` 8 | docker build -t py3-threads . 9 | ``` 10 | 11 | To access the shell, run a container using this command: 12 | 13 | ``` 14 | docker run -it py3-threads bash 15 | ``` 16 | 17 | All of the source code will be located in the `/src` directory in the container. To sync this directory with your working directory when you run the container, use this command: 18 | 19 | ``` 20 | docker run -it --mount type=bind,source=$(pwd),target=/src py3-threads bash 21 | ``` 22 | 23 | If you have `make` you can use the following command to automate the build and run commands: 24 | 25 | ``` 26 | make run 27 | ``` 28 | 29 | ## Tips and Notes 30 | 31 | * To view the threads as a program is running with `htop`: `htop` -> `F2` -> `Show custom thread names` (tree view is nice, too) 32 | * To view the threads as a program is running `ps`: `ps -eLF` 33 | * Show information about the current thread: `threading.current_thread()` 34 | * *Green threads* are scheduled by the Python process, as opposed to a system-level thread scheduled by the OS. 35 | -------------------------------------------------------------------------------- /example_file_processor.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import os 3 | import random 4 | from queue import Queue 5 | from collections import Counter 6 | import time 7 | 8 | class FileProcessor: 9 | def __init__(self, directory, file_pattern, max_threads=4): 10 | self.directory = directory # Directory containing the files to process 11 | self.file_pattern = file_pattern # File extension or pattern to match 12 | self.max_threads = max_threads # Maximum number of threads to use 13 | self.queue = Queue() # Queue to hold files to be processed 14 | self.lock = threading.Lock() # Lock for thread-safe updates to shared data 15 | self.total_word_count = Counter() # Shared counter for word frequencies across all files 16 | 17 | def process_file(self, filepath): 18 | # Process a single file and return its word count 19 | time.sleep(random.uniform(0.1, 0.5)) # Simulate latency 20 | with open(filepath, 'r') as file: 21 | # Read the file and split into words 22 | word_count = Counter(file.read().split()) 23 | return word_count 24 | 25 | def worker(self): 26 | while True: 27 | # Get a file path from the queue 28 | filepath = self.queue.get() 29 | 30 | # None is our signal to stop the worker 31 | if filepath is None: 32 | break 33 | 34 | print(f"Processing: {filepath}") 35 | 36 | # Process the file and get its word count 37 | file_word_count = self.process_file(filepath) 38 | 39 | # Update the total word count in a thread-safe manner 40 | with self.lock: 41 | self.total_word_count.update(file_word_count) 42 | 43 | # Mark the task as done 44 | self.queue.task_done() 45 | 46 | def run(self): 47 | # Populate the queue with files to process 48 | for filename in os.listdir(self.directory): 49 | if filename.endswith(self.file_pattern): 50 | self.queue.put(os.path.join(self.directory, filename)) 51 | 52 | # Create and start the worker threads 53 | threads = [] 54 | for _ in range(self.max_threads): 55 | t = threading.Thread(target=self.worker) 56 | t.start() 57 | threads.append(t) 58 | 59 | # Wait for all tasks in the queue to be completed 60 | self.queue.join() 61 | 62 | # Stop the workers by sending them None 63 | for _ in range(self.max_threads): 64 | self.queue.put(None) 65 | 66 | # Wait for all threads to finish 67 | for t in threads: 68 | t.join() 69 | 70 | return self.total_word_count 71 | 72 | def create_sample_logs(directory, num_files=10, lines_per_file=100): 73 | """ 74 | Create a directory with sample log files containing random words. 75 | 76 | :param directory: The directory to create and fill with log files 77 | :param num_files: Number of log files to create 78 | :param lines_per_file: Number of lines in each log file 79 | """ 80 | # Ensure the directory exists 81 | os.makedirs(directory, exist_ok=True) 82 | 83 | # List of sample words to use in log files 84 | words = ["INFO", "DEBUG", "WARNING", "ERROR", "CRITICAL", "GET", "POST", "PUT", "DELETE", 85 | "database", "server", "client", "network", "file", "user", "system", "process", 86 | "memory", "CPU", "disk", "application", "service", "module", "function", "class"] 87 | 88 | # Create log files 89 | for i in range(num_files): 90 | filename = os.path.join(directory, f"sample_log_{i+1}.log") 91 | with open(filename, 'w') as file: 92 | for _ in range(lines_per_file): 93 | # Generate a random log line 94 | log_line = " ".join(random.choices(words, k=random.randint(5, 15))) 95 | file.write(log_line + "\n") 96 | 97 | print(f"Created {num_files} sample log files in {directory}") 98 | 99 | if __name__ == "__main__": 100 | # Generate the sample log files 101 | create_sample_logs("./logs", num_files=1000, lines_per_file=1000) 102 | 103 | # Create a FileProcessor instance 104 | processor = FileProcessor("./logs", ".log", max_threads=40) 105 | 106 | # Run the processor and get the total word count 107 | word_count = processor.run() 108 | 109 | # Print the top 10 most common words 110 | print("Top 10 words:") 111 | for word, count in word_count.most_common(10): 112 | print(f"{word}: {count}") -------------------------------------------------------------------------------- /example_multiprocessing.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | import time 3 | 4 | COUNT = 50000000 5 | def countdown(n): 6 | while n>0: 7 | n -= 1 8 | 9 | if __name__ == '__main__': 10 | processes = 2 11 | pool = Pool(processes=processes) 12 | start = time.time() 13 | r1 = pool.apply_async(countdown, [COUNT//processes]) 14 | r2 = pool.apply_async(countdown, [COUNT//processes]) 15 | pool.close() 16 | pool.join() 17 | end = time.time() 18 | print('Time taken in seconds (multiprocessing) -', end - start) 19 | 20 | # Single process 21 | start = time.time() 22 | countdown(COUNT) 23 | end = time.time() 24 | print('Time taken in seconds (default processing) -', end - start) 25 | -------------------------------------------------------------------------------- /example_spinner_thread.py: -------------------------------------------------------------------------------- 1 | # Source: https://learning.oreilly.com/library/view/fluent-python-2nd/9781492056348/ch19.html#idm46582392974816 2 | import itertools 3 | import time 4 | import requests 5 | from threading import Event, Thread 6 | 7 | 8 | def spin(msg: str, done: Event) -> None: 9 | """ 10 | This function will run in a separate thread. The done argument is an instance of threading.Event, a simple object to synchronize threads. 11 | """ 12 | for char in itertools.cycle(r'\|/-'): 13 | status = f'\r{char} {msg}' 14 | print(status, end='', flush=True) 15 | if done.wait(.1): 16 | break # Exit the infinite loop. 17 | blanks = ' ' * len(status) 18 | print(f'\r{blanks}\r', end='') 19 | 20 | def slow() -> int: 21 | """ 22 | slow() will be called by the main thread. Imagine this is a slow API call over the network. Calling sleep blocks the main thread, but the GIL is released so the spinner thread can proceed. 23 | """ 24 | time.sleep(3) 25 | url = "https://www.random.org/integers/?num=1&min=1&max=100&col=1&base=10&format=plain&rnd=new" 26 | response = requests.get(url) 27 | return response.text.strip() 28 | 29 | def supervisor() -> int: 30 | done = Event() 31 | spinner = Thread(target=spin, args=('Thinking...', done)) 32 | spinner.start() 33 | result = slow() 34 | done.set() 35 | spinner.join() 36 | return result 37 | 38 | def main() -> None: 39 | result = supervisor() 40 | print(f'Answer: {result}') 41 | 42 | if __name__ == '__main__': 43 | main() -------------------------------------------------------------------------------- /example_webscraper.py: -------------------------------------------------------------------------------- 1 | import threading 2 | import requests 3 | from queue import Queue 4 | from urllib.parse import urljoin 5 | from bs4 import BeautifulSoup 6 | 7 | class WebScraper: 8 | def __init__(self, base_url, max_threads=5): 9 | self.base_url = base_url 10 | self.max_threads = max_threads 11 | self.queue = Queue() 12 | self.results = [] 13 | self.visited = set() 14 | 15 | def scrape_page(self, url): 16 | response = requests.get(url) 17 | soup = BeautifulSoup(response.text, 'html.parser') 18 | 19 | # Extract title 20 | title = soup.title.string if soup.title else "No title" 21 | 22 | # Extract first paragraph 23 | first_p = soup.find('p') 24 | first_paragraph = first_p.text if first_p else "No paragraph found" 25 | 26 | # Extract links 27 | links = [urljoin(self.base_url, a['href']) for a in soup.find_all('a', href=True)] 28 | 29 | return { 30 | "url": url, 31 | "title": title, 32 | "first_paragraph": first_paragraph, 33 | "links": links 34 | } 35 | 36 | def worker(self): 37 | while True: 38 | url = self.queue.get() 39 | if url is None: 40 | break 41 | if url not in self.visited: 42 | self.visited.add(url) 43 | print(f"Scraping: {url}") 44 | page_data = self.scrape_page(url) 45 | self.results.append(page_data) 46 | for link in page_data["links"]: 47 | if link.startswith(self.base_url) and link not in self.visited: 48 | self.queue.put(link) 49 | self.queue.task_done() 50 | 51 | def run(self): 52 | self.queue.put(self.base_url) 53 | threads = [] 54 | for _ in range(self.max_threads): 55 | t = threading.Thread(target=self.worker) 56 | t.start() 57 | threads.append(t) 58 | 59 | self.queue.join() 60 | 61 | for _ in range(self.max_threads): 62 | self.queue.put(None) 63 | for t in threads: 64 | t.join() 65 | 66 | return self.results 67 | 68 | if __name__ == "__main__": 69 | scraper = WebScraper("https://python.org", max_threads=50) 70 | results = scraper.run() 71 | print(f"Scraped {len(results)} pages") 72 | for page in results[:5]: 73 | print(f"URL: {page['url']}") 74 | print(f"Title: {page['title']}") 75 | print(f"First Paragraph: {page['first_paragraph'][:100]}...") # Truncate for brevity 76 | print(f"Links found: {len(page['links'])}") 77 | print("---") -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /training.py: -------------------------------------------------------------------------------- 1 | # This modules defines variables and functions used as examples in this training. 2 | import requests 3 | import time 4 | import threading 5 | import ssl 6 | from datetime import datetime as dt 7 | 8 | 9 | WEBSITES = [ 10 | 'http://mfa.go.th/', 11 | 'http://www.antarctica.gov.au/', 12 | 'http://www.mofa.gov.la/', 13 | 'http://www.presidency.gov.gh/', 14 | 'https://www.aph.gov.au/', 15 | 'https://www.argentina.gob.ar/', 16 | 'https://www.fmprc.gov.cn/mfa_eng/', 17 | 'https://www.gcis.gov.za/', 18 | 'https://www.gov.ro/en', 19 | 'https://www.government.se/', 20 | 'https://www.india.gov.in/', 21 | 'https://www.jpf.go.jp/e/', 22 | 'https://www.oreilly.com/', 23 | 'https://www.parliament.nz/en/', 24 | 'https://www.presidence.gov.mg/', 25 | 'https://www.saskatchewan.ca/' 26 | ] 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | def visit_website(url): 38 | """Makes a request to a url and prints the status code and elapsed time""" 39 | try: 40 | response = requests.get(url) 41 | print(f'{url} returned {response.status_code} after {response.elapsed} seconds') 42 | return response.elapsed 43 | except Exception as e: 44 | print(f'Failed to connect to {url}') 45 | pass 46 | 47 | 48 | 49 | 50 | 51 | 52 | def log_website(url): 53 | """Makes a request to a url and writes a log""" 54 | basename = url.split('.')[1] 55 | while True: 56 | time.sleep(0.25) 57 | with open(f'{basename}.log', 'a') as log: 58 | log.write(f'{dt.now()}\n') 59 | 60 | 61 | 62 | 63 | 64 | class Account(): 65 | def __init__(self): 66 | self.balance = 0 67 | 68 | def __repr__(self): 69 | return f'Current balance is {self.balance}' 70 | 71 | def deposit(self, amount): 72 | print(f'Depositing {amount}') 73 | # Simulates a database read and write 74 | state = self.balance # 0 75 | time.sleep(0.1) 76 | state += amount 77 | self.balance = state # 100 78 | 79 | def withdrawal(self, amount): 80 | print(f'Withdrawing {amount}') 81 | # Simulates a database read and write 82 | state = self.balance # 100 or 0 83 | time.sleep(0.1) 84 | state -= amount # 50, -50 85 | self.balance = state 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | class ThreadSafeAccount(): 97 | def __init__(self): 98 | self.balance = 0 99 | self.lock = threading.Lock() # Give each account a Lock 100 | 101 | def __repr__(self): 102 | return f'Current balance is {self.balance}' 103 | 104 | def deposit(self, amount): 105 | print(f'Depositing {amount}') 106 | 107 | # Limit access to shared data to only one thread at a time 108 | with self.lock: 109 | state = self.balance 110 | state += amount 111 | self.balance = state 112 | 113 | def withdrawal(self, amount): 114 | print(f'Withdrawaling {amount}') 115 | 116 | # Limit access to shared data to only one thread at a time 117 | # is self.lock acquired? 118 | # once self.lock is released, 119 | with self.lock: 120 | state = self.balance 121 | state -= amount 122 | self.balance = state 123 | -------------------------------------------------------------------------------- /tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/scalewithlee/python-threading/afc66a53f65d02bdc9041b35ee0757a6ecdf4e1a/tree.png --------------------------------------------------------------------------------