CPython's Global Interpreter Lock (GIL) ensures that only one thread executes Python bytecode at a time, simplifying memory management but limiting CPU-bound multithreading parallelism. For CPU-bound work the common remedies are multiprocessing or native extensions; threading remains well-suited to I/O-bound tasks, where the GIL is released during waits. The choice between threading and multiprocessing hinges on this trade-off: threads share memory and are constrained by the GIL, while processes have separate memory spaces and run truly in parallel.
The `asyncio` library provides single-threaded asynchronous concurrency through coroutines — functions defined with `async def` that can pause at `await` points. `await` yields control until the awaitable completes. The event loop is the runtime that schedules and runs coroutines; `asyncio.run()` serves as the top-level entry point. `asyncio.create_task()` schedules a coroutine as a Task, and `asyncio.gather()` runs multiple awaitables concurrently and collects their results. `asyncio.Queue` provides an async-safe queue for producer/consumer patterns. Thread safety and async safety are distinct concerns: locks and queues for threads address one scheduling model, while asyncio primitives address another.
Beyond asyncio, `concurrent.futures` offers a high-level API with `ThreadPoolExecutor` and `ProcessPoolExecutor`. On Unix systems, `os.fork()` creates a child process, though the `multiprocessing` module is generally preferred for portability. The `subprocess` module spawns and manages external processes. Together these tools let developers choose the right concurrency model — threads or coroutines for I/O-bound work, processes for CPU-bound parallelism.