Master the fundamental concepts of cpython internals through this focused micro-challenge.
You have read the whole brief, and the concepts above stay free on every task. Writing and running the code needs a plan.
Three hints are available for this task, revealed one at a time inside the code workspace so you can struggle productively before seeing them.
Every task includes starter code, theory, and hidden tests so you can implement and verify locally in the browser.
How it worksCPython's GIL is a mutex that lets only one thread execute Python bytecode at a time per process. It simplifies object refcounting but limits CPU-bound parallelism across threads.
The GIL protects CPython's internal structures during concurrent access. Reference count updates, object allocation, and many C API calls assume the GIL is held.
CPU-bound C extensions can release it around long native work:
cLoading…
When the GIL helps versus hurts:
Py_BEGIN_ALLOW_THREADSI/O-bound threads still benefit because blocking syscalls release the GIL while waiting. CPU-bound Python threads on multiple cores, however, mostly take turns holding one lock.
For example, two threads each computing Fibonacci in pure Python rarely achieve 2x throughput; one thread runs bytecode while the other waits.
Extensions that call back into Python API while holding native locks can deadlock with the GIL. Release the GIL before long C work, re-acquire before touching PyObject* unless you know the object is immortal.
This exercise asks you to explain GIL behavior and write a C extension skeleton that releases the lock during heavy work. You will document when threads help (I/O) versus when they do not (CPU-bound Python code).
Simulate how CPython's GIL shares one interpreter between threads. Only the GIL holder can run Python bytecode (cpu work). Blocking I/O and C code wrapped in Py_BEGIN_ALLOW_THREADS/Py_END_ALLOW_THREADS (native work) release the GIL, so they overlap with other threads. When another thread is waiting, the holder is forced to drop the GIL after the switch interval (sys.setswitchinterval, 5 ms by default). The simulation runs in 1 ms steps on a machine with enough cores, so real threads are never needed.
cLoading…
At most 8 threads can be defined.
At time 0, the threads start their first segment in definition order. A thread that starts or reaches a cpu segment without holding the GIL joins the back of a FIFO wait queue. Then each millisecond t:
cpu work. Threads in io/native progress without the GIL. Queued threads accumulate waited time.t+1, in definition order, each thread whose segment is complete moves to its next one:
io/native, or finishing, releases the GIL;cpu joins the queue;cpu segment keeps the GIL.interval ms since acquiring it, and the queue is not empty, it goes to the back of the queue and the GIL is free.cLoading…
idle while nobody holds it.1 forced switch and 1 thread in the singular.Errors:
interval: 1..100 msthread N: bad segment KIND MSthread N: expected ':' before the segmentsthread N: no segmentsthread N: at most 8 threadsrun: no threadsInput:
cLoading…
Output:
cLoading…
Hidden tests cover I/O-bound threads that overlap, a native (GIL-released) extension against the same work done in bytecode, a different switch interval with the timeline, a thread with only I/O, and input errors.