In Part 2, you will learn how semaphores limit concurrent work when many tasks share a restricted resource.
You will:
- explain the difference between a mutex and a semaphore
- use
threading.Semaphoreto limit concurrent work - simulate many workers sharing only a few available passes
- limit web-style calls to 4 active calls at a time
- protect shared result-file writing
From the session6 folder:
- Activate your virtual environment.
source .venv/bin/activateWindows PowerShell:
.venv\Scripts\Activate.ps1- Install dependencies if needed:
pip install -r requirements.txt- semaphore: a counter-based lock that allows up to N workers at the same time
- permit: one available slot in a semaphore
- concurrency limit: the maximum number of workers allowed to do something at once
- I/O-bound work: work that spends time waiting on input/output, such as network calls
A semaphore is useful when several workers are allowed to run at the same time, but there is still a limit.
Example:
import threading
limit = threading.Semaphore(4)
with limit:
# at most 4 threads can run this block at the same time
print("limited shared work")A mutex is like a room with one key. A semaphore is like a room with a fixed number of passes.
Checkpoint question:
If you have 40 tasks but a semaphore limit of 4, how many tasks can be inside the protected block at once?
Show answer
At most 4 tasks can be inside the protected block at the same time. The other tasks wait until one of the 4 leaves.
Before simulating web calls, try a tiny example where five workers share only two available passes.
Create this warm-up file:
session6/solutions/exercise-06-00-semaphore.pyAdd this code:
from concurrent.futures import ThreadPoolExecutor
import threading
import time
door_limit = threading.Semaphore(2)
def enter_room(worker_name):
print(f"{worker_name} is waiting to enter")
with door_limit:
print(f"{worker_name} entered")
time.sleep(1)
print(f"{worker_name} left")
if __name__ == "__main__":
workers = ["Worker A", "Worker B", "Worker C", "Worker D", "Worker E"]
with ThreadPoolExecutor(max_workers=5) as executor:
for worker in workers:
executor.submit(enter_room, worker)Run:
python3 solutions/exercise-06-00-semaphore.pyWhat this shows:
- Five workers are submitted.
- Only two workers can enter the protected block at the same time.
- When one worker leaves, another waiting worker can enter.
Show explanation
threading.Semaphore(2) starts with two available permits. Each worker takes one permit when entering with door_limit: and returns it when leaving the block.
Create this file and complete it:
session6/solutions/exercise-06-02.pyYour program should:
- Emulate 40 web calls.
- Use
requeststo make a small HTTP request for each task. - Use a semaphore so only 4 calls are active at the same time.
- Save one result line per call into
request_results.txt. - Print clear start/finish messages.
- Measure total runtime with
time.perf_counter().
Tip
httpbin.org is a small HTTP testing service. The /delay/1 endpoint waits about 1 second before responding, which makes it useful here because you can clearly see the semaphore limiting how many calls run at the same time.
Use this URL pattern:
url = f"https://httpbin.org/delay/1?request={request_id}"Network note:
- This exercise depends on internet access (
httpbin.org). - If a request fails, record the failure and continue.
- If your network is unavailable, you may temporarily replace the request with
time.sleep(1)while testing the semaphore behavior.
Suggested skeleton:
from concurrent.futures import ThreadPoolExecutor
import threading
import time
import requests
call_limit = threading.Semaphore(4)
write_lock = threading.Lock()
def fetch(request_id):
url = f"https://httpbin.org/delay/1?request={request_id}"
# TODO: use call_limit to allow only 4 active requests
# TODO: call requests.get(url, timeout=10)
# TODO: write one result line to request_results.txt
...
if __name__ == "__main__":
start = time.perf_counter()
# TODO: clear request_results.txt
# TODO: run 40 fetch tasks using ThreadPoolExecutor
...
end = time.perf_counter()
print(f"Total time: {end - start:.2f}s")Run:
python3 solutions/exercise-06-02.pyMinimum completion checklist:
- Exactly 40 tasks are attempted.
- At most 4 requests are active at the same time.
- Results are written to
request_results.txt. - Failures are recorded instead of crashing the whole program.
Show hint
Use with call_limit: around the actual request. Use a separate write_lock for writing to the output file, because limiting requests and protecting file writes are two different coordination problems.
Show expected idea
With 40 one-second request-like tasks and a limit of 4, the runtime should often feel closer to 10 waves of work than 40 fully serial waits. Real network timing varies, so focus on whether the limit is respected and all results are recorded.
quizmd quizzes/python-session-06-part-02-quiz.md