There is a lot of misinformation about Python and it’s handling of concurrency and parallelism that leads to a lot of confusion for beginners approaching the subject for the first time. This blog post is about concurrency in Python in general. Let us first understand the difference between concurrency and parallelism.
Concurrent execution means that the start and end times of two or more independent segments of the program overlap. Parallelism means that there is simultaneous computation happening on two or more segments of the code at the same time. Parallelism necessitates the existence of multi-core CPUs or some other kind of redundancy in hardware, while concurrency is a more high level construct that makes no such demand. It follows that parallelism implies concurrency but not vice versa. Look at the illustration below for more clarity.
Which of the two above does Python support? Depends on the implementation. The implementation of your program? No, the implementation of the language itself. Let me explain.
A language by itself is only a specification - syntax and grammar. It does not mandate how it will be converted to machine code at all - via compilation or interpretation or something else. Traditional interpretation, JIT compilation, JIT interpretation and AOT compilation are some of the choices. These decisions are a part of the implementation of the language. As it turns out, Python has many different implementations with CPython, Jython, PyPy and IronPython being the dominant ones. CPython is the default implementation that Python comes bundled with. Execute the following code on your Python interpreter import platform; platform.python_implementation()
to see this for yourself.
Now, CPython has some interesting quirks - the developers decided against allowing multiple threads to use the interpreter parallely. This fact is known as the GIL and is lamented by many users of Python - because it leads to threads not being able to leverage multi-core processors and as a result users do not get the speed boost that they get in other languages like Java and C. In practice, the overhead of scheduling the threads on the same processor and managing access to the lock makes multithreading actually slow down your program! So multi-threading loses all value for CPU-bound tasks although it is still useful for IO-bound tasks.
There are important reasons why it isn’t trivial to replace the GIL in CPython. Some implementations of Python like Jython and IronPython do not have the GIL. But they offer a tiny fraction of the library support that CPython provides users. Jython does not even support NumPy and SciPy. In practice, GIL isn’t too big a problem at all. Because even with the GIL, there are ways to achieve parallelism as we will see below. This is also why CPython remains the dominant implementation for general users.
One way to achieve speed boosts on CPU-bound programs is through using multiple processes through the multiprocessing
module available in Python. Why does this not suffer from the same problems as multi-threading? Simple, because each process has it’s own GIL. But why do developers lament about GIL if this is a perfectly functional solution? That’s because using multiple processes is not really the same as using multiple threads.
These things must be kept in mind before one implements a parallel solution in Python.