Python: Multithreading, Multiprocessing, and the GIL Explained

One of the many things that make Python such a popular and powerful language is that you can easily divide your code into multiple concurrent threads rather easily, take this for example:

from threading import Thread

def thread_func(n):
    print(f"I'm thread number {n}!")

for t in range(4):
    t = Thread(target=thread_func, args=[t])
    t.start()

4 threads! Easy, right?

Well, not exactly.

Threading Vs. Processing

Note that the two are not interchangeable. I might go into a rather technical comparison between the two later, but for now let’s keep things rather straightforward and simple.

Processes

A process is the container for all the resources and state a program needs to run. A process defines a memory allocation, priority to tell the OS how often it should be allowed to run, and holds handles to any files that you’re interacting with.

A process contains at least one thread, but can create more.

Threads

A thread is the code within a process that the OS can schedule to be executed. Threads share the memory space of a process (and therefore, other threads in that process), and really only keep to themselves the context needed to start and stop execution many times a second that many modern OS will do.

Or put simply: A process is a program, and a thread is a piece of that program that can run in parallel with the other pieces.

Why Does That Matter?

Python does make a distinction, and indeed you can replace the threading library with multiprocessing and the call to Thread() with a call to Process() and it’d still work.

So, what are the differences?

  • A thread is less resource-intensive than a process since it’s not creating another set of bookkeeping structures
  • Threads can share memory a lot easier than processes, which require some hoops to jump through to achieve that
  • A thread has a defined lifetime: its parent process. It terminates when the process terminates. A process has no such lifetime, and needs to explicitly end by itself. (Though most threads also have a defined end somewhere)
  • Processes bypass the GIL

The GIL

The Python Global Interpreter Lock, or GIL for short, is a part of the python engine that prevents more than one thread from interpreting and executing Python code at the same time. In python, all your program’s threads share the same interpreter, meaning that only one thread can really run at any given time, despite their definition. If you want true parallelism, you should create a new process, because creating a process also spawns another interpreter, and therefore, another GIL for it. Since they’re completely separate, there is no locking going on, and therefore, can execute at the same time. The only real downside? More memory used, and another process in the OS’s process table.