Concurrency and Parallelism: What is the difference?

A fresh simple way to understand these complex topics

Kieron Spearing
Towards Data Science

--

Image made by myself using diagrams.net

In my current position and in many articles, I have heard these expressions thrown around a lot, however, when I first started I didn’t truly understand what they meant and naturally I felt that this was unacceptable.

Having felt this way I immediately decided that I could not put up with that and started to delve deeper into them to understand why they are important to computer science and discovered how to learn them using simple examples.

In this post, I will be going over these concepts at a basic level as well as a few simple implementations in Python that helped me understand them better.

If you wish to struggle to understand them after reading this post I would suggest re-implementing the above examples synchronously with a simple for loop to see the difference yourself.

What is Concurrency and Parallelism?

When you develop complex systems that have a lot of tasks, you will eventually come to a point where performing all of these actions synchronously isn’t efficient enough for your needs.

Concurrency and parallelism are mechanisms that were implemented to allow us to handle this situation either by interweaving between multiple tasks or by executing them in parallel.

On the surface these mechanisms may seem to be the same however, they both have completely different aims.

Concurrency

The goal of concurrency is to prevent tasks from blocking each other by switching among them whenever one is forced to wait on an external resource. A common example of this is handling multiple network requests.

One way to do this would be to launch a single request, wait for the response and then launch the following request and repeat until you have handled every request. The problem with this is it is very slow and inefficient.

The better way would be to launch every request simultaneously, then switch among them as you receive the responses. By doing this we eliminate the time spent waiting for the responses.

Parallelism

If you have ten workers you wouldn’t want one of them doing all the work while the other nine sit around and do nothing.

Instead, you would split the work among all the workers, that way not only would the work be done faster but also the workers would do less.

Parallelism takes the same concept and applies it to the hardware resources at hand. It is all about maximizing the use of said resources by launching processes or threads that make use of all the CPU cores that the computer possesses.

Both of these concepts are extremely useful for handling multiple tasks simultaneously although you need to pick the correct method for your particular needs.

Concurrency is amazing for tasks that depend greatly on external resources, while parallelism is amazing for many CPU-intensive tasks.

How do we work with these concepts in Python?

Python provides us with mechanisms to implement concurrency and parallelism, for concurrency we have threading and async whereas for parallelism we can make use of multiprocessing.

Oftentimes these are seen as scary topics and in some ways they are, I however found that taking a relatively simple example of each implementation and playing with them in many ways is the best way to approach it before taking the deep dive into the theoretical side.

I highly recommend that once finished with this post, you take the concepts and the examples I have left below and play with them until you feel comfortable enough to try to implement them in different ways yourself.

Threading

Having said to not worry too much about the theory before practicing, we do need to have a basic idea about what a thread is to get started with threading.

A thread is similar to a sequential program in that it has a beginning, an execution sequence, and an end. At any time during the runtime of the thread, there is a single point of execution however, a thread is not a program, it cannot run on its own.

Essentially a thread is a separate flow of execution, where you can take one or more functions and execute them independently of the rest of the program. You can then work with the results, usually by waiting for all threads to run to completion.

There are two main ways in which threading is handled in Python, either with the threading library or with the ThreadPoolExecutor created as a context manager, this is the simplest way to manage the creation & destruction of the pool.

If you take a look at the above example we are simply creating a function that is being launched in separate threads that simply starts the thread and then sleeps, simulating some external wait time.

In the main function, you can see how I have implemented both the above methods, the first in lines 9-19 and the second in lines 23 & 24.

While both methods are fairly simple, it is quite obvious which implementation requires less effort from our side.

This is a rather simplified example however, it does help to understand the theory. A more realistic simple example of this could be the following script.

In this snippet, we are using threading to read the data from multiple URLs simultaneously by executing multiple instances of the thread_function() and storing the results in a list.

As you can see using the ThreadPoolExecutor makes it easy to handle the required threads. Although this is a small example we could submit many more URLs using this without having to wait for each response.

From the above examples, we can see that threads are a convenient and well-understood way to handle tasks that wait on other resources, we also see that Python’s standard library comes with high-level implementations to make it even easier.

However, we must also remember that the Python runtime divides its attention between the threads so that it can manage them correctly, thus this is not suitable for CPU-intensive work.

Coroutines and asyncio

Coroutines are a different way to execute functions concurrently by way of special constructs rather than system threads.

Forgive me once more for getting you to dip the tips of your toes into some basic theoretical knowledge on the topic but it is essential to know what a coroutine is to understand what is happening as we implement this in Python.

A coroutine is a general control structure whereby flow control is cooperatively passed between two different routines without returning, it is not threading, nor is it multiprocessing. Neither is it built on top of either of these, it is a single-threaded, single-process design using cooperative multitasking.

Essentially while threading takes multiple functions and launches them on separate threads, asyncio operates on a single thread and allows the program’s event loop to communicate with multiple tasks to allow each to take turns running at the optimal time.

We implement this in Python by using asyncio which is a Python package that provides a foundation and API for running and managing coroutines along with the async and await keywords.

In a similar fashion to earlier you can see how we implement a very basic example:

I believe this is a much more clean and simple way to implement concurrency in Python. However, it is much newer to the language and thus less used than threading.

Once more I can take the exact same example related to reading the data from multiple URLs and implement it using asyncio.

At first, it may come across as a bit more complicated but I think it is a lot cleaner, more explicit, and easier to understand than threading.

This is because coroutines make it clear in the program’s syntax which functions run side by side, while with threads any function can be run in a thread.

They also are not bound by architectural limitations as threads are and require less memory due to the fact that it runs on a single thread.

The only downside is that they do require the code to be written in their own syntax and it doesn’t mingle well with synchronous code, as well as the fact that they don’t allow CPU-intensive tasks to run efficiently side-by-side.

Multiprocessing

Multiprocessing is the mechanism that allows you to run many CPU-intensive tasks in parallel by launching multiple, independent instances of the Python interpreter.

Each instance receives the code and data required to run the task in question and runs independently on its own thread.

Although the above examples are not amazing in regards to the kinds of tasks you would want to run using multiprocessing, we can implement them once more in a similar fashion so that we can see how they differ from one another.

The above being the most basic process we could replicate with multiprocessing, and the below reimplementing the exact same functionality of reading data from multiple URLs simultaneously.

In this snippet above the Pool() object represents a reusable group of processes that we are mapping the iterable to distribute between each instance of the function.

The one massive advantage this provides is the fact that each operation is run operating on a separate Python runtime and a full CPU core, allowing us to run CPU-intensive processes simultaneously.

One of the downsides is that each subprocess needs a copy of the data it works with sent to it from the main process and generally they return data to the main process.

As I said earlier, I have been working with an incredibly simple example.

However, I find the best way to learn and become comfortable working with new complex concepts in a language is to make it extremely simple at first and slowly but surely peel back the simplicity and expose the complex details below.

I hope this has helped you find a way to approach these vast topics in a relatively simple way.

--

--

I am a Full-Stack Developer at StyleSage and a Food enthusiast with 2 years experience in technology and 7 years experience working in Michelin Star restaurants