Python Concurrency Quiz: GIL, Threads, Processes and asyncio

Test your Python concurrency with 12 questions and three programs on the GIL, threads and locks, queues, executors, multiprocessing and asyncio gather.

  • Course: Python study plan
  • Module: Concurrency — threads, processes and asyncio
  • Kind: Checkpoint — cleared at 70%
  • Reading time: 25 min
  • Runtime: CPython 3.11

Checkpoint — Concurrency is the checkpoint that closes the Concurrency — threads, processes and asyncio module: a graded quiz and whole-program exercises, passed at 70%.

Instructions

This checkpoint covers the whole module: the GIL and the three models, threads with Lock, Queue and Event, the race and its fix, concurrent.futures executors with ordered map and unordered as_completed, multiprocessing with pickling and the main guard, and asyncio from coroutines and gather through TaskGroup, semaphores, queues and bridging to blocking code — plus the determinism rule that every judged program obeys.

How it works. Twelve questions and three programs. You need 70% on the questions and every program accepted to clear the module. You can retake it as often as you like; your best score counts.

Before you start, make sure you can answer these from memory:

  • Why do threads not speed up a CPU-bound loop in CPython, and what does speed it up?
  • What are the three bytecodes behind counter += 1, and where can the switch happen?
  • What is the difference between executor.map and as_completed in the order of results?
  • Why must code that starts processes live under if __name__ == "__main__":?
  • What happens when a coroutine is called without await?
  • What does asyncio.gather return, and in what order?
  • What is the one way to make a threaded program's output deterministic?

The three programs are a threaded word counter whose workers fill per-thread slots and are merged after join, a pipeline of workers reading a Queue with sentinels whose per-worker results are merged in order, and an asyncio job runner with a semaphore that records a deterministic trace through sleep(0) yields and reports results with gather.

Common questions

Why do threads not speed up a CPU-bound loop in CPython?

The GIL lets only one thread execute Python bytecode at a time, so CPU-bound threads take turns rather than run in parallel. Processes, through ProcessPoolExecutor or multiprocessing, or a C extension that releases the GIL, do speed it up.

What does asyncio.gather return, and in what order?

A list of the awaitables' results in the order the arguments were given, regardless of which finished first. An exception propagates to the caller unless return_exceptions=True collects it as a value in the list.

What is the one way to make a threaded program's output deterministic?

Give every worker its own result slot, join all threads before reading, and print the aggregated, ordered results from the main thread afterwards. A worker that prints directly interleaves with the others in an arbitrary order.

Exercises

Threaded word counter

Read T on the first line, then text lines until EOF. Thread i handles lines[i::T]: it lowercases each line, splits on whitespace, strips .,;:!? from each word, and counts the words into its own collections.Counter in slots[i]. Join the threads, merge the counters, and print total <number of words> followed by the top three words as <word> <count> ordered by count descending then word ascending (fewer if there are fewer distinct words).

Input: T, then lines of text. Output: the total, then up to three lines.

2
the cat sat on the mat
the dog sat
a cat

prints

total 11
the 3
cat 2
sat 2

Queue pipeline by index

Read W on the first line, then text lines until EOF. Put (index, line) jobs on a queue.Queue followed by W sentinels. Each of W worker threads takes jobs and, for each, computes "<index>: <words> words, <chars> chars, longest <word>" where words is the whitespace word count, chars is len(line) and longest is the first longest word; it appends (index, text) to a shared results list under a Lock. Join the workers, sort the results by index, and print each text.

Input: W, then lines. Output: one line per input line, in input order.

2
the quick brown fox
jumps over
the lazy dog

prints

0: 4 words, 19 chars, longest quick
1: 2 words, 10 chars, longest jumps
2: 3 words, 12 chars, longest lazy

Async job runner with a trace

Read limit on the first line, then lines <name> <steps> until EOF. Write async def run(name, steps) that, inside async with sem: for a Semaphore(limit), appends start <name> to a shared trace list, awaits asyncio.sleep(0) steps times, appends done <name>, and returns (name, sum(range(1, steps + 1))). Gather every job in input order; print each trace line, then <name> -> <value> for each result in gather order.

Input: limit, then one job per line. Output: the trace, then the results.

2
a 2
b 1
c 1

prints

start a
start b
done b
done a
start c
done c
a -> 3
b -> 1
c -> 1

In this module: Concurrency — threads, processes and asyncio

← asyncio patterns — TaskGroup, queues, semaphores, async iteration and bridging · The testing mindset — what to test, how to arrange it, and designing for testability →