Producer-Consumer example comparing threading and asyncio
In this post, we will compare the producer-consumer example written in using multi-threading approach with the one written using asyncio approach.
Producer consumer is a common problem for those wanting to learn about concurrency and it tends to be the one of the first problem that we learn about, while experimenting with any kind of concurrency code.
In this post, we will compare the producer-consumer example written in using multi-threading approach with the one written using asyncio approach.
Multi-threading Approach
Multi-threading on python sucks. Well, that is the popular opinion. I beg to differ, though. Of course, the fact that multi-threading in python is incapable of using the multi-cores in current generation of cpus, it doesn’t mean that multi-threading was not useful in the days of single core cpus. It is like fighting with one hand tied behind your back. But, you can still fight.
Let’s look at the code.
import threading, time
from multiprocessing import Queue
def producer(q, n):
for item in range(n):
q.put(item)
print(f"[Produced] {item}")
time.sleep(1)
print("[Producer] Done")
q.put(None)
def consumer(q):
while True:
item = q.get()
if item is None:
break
print(f"[Consumer] {item}")
time.sleep(1)
print("[Consumer] Done")
q = Queue()
t1 = threading.Thread(target=lambda: producer(q, 10))
t2 = threading.Thread(target=lambda: consumer(q))
t1.start()
t2.start()
The output shows the non sequential execution as a result of concurrently running the program.
python3.6 threading_example.py
[Produced] 0
[Produced] 1
[Consumer] 0
[Consumer] 1
[Produced] 2
[Produced] 3
[Consumer] 2
[Produced] 4
[Consumer] 3
[Produced] 5
[Consumer] 4
[Produced] 6
[Consumer] 5
[Produced] 7
[Consumer] 6
[Consumer] 7
[Produced] 8
[Consumer] 8
[Produced] 9
[Consumer] 9
[Producer] Done
[Consumer] Done
The code in general is quite straight forward. There’s a producer method and a consumer method. We give those methods to Threads created using threading module and the execution is begun.
Asyncio Approach
Asyncio was introduced with Python 3.4 and honestly, it took some getting use to. I had to learn nodejs before getting the hang of async appoach to concurrency. It is not a silver bullet claimant, by any means. It is great though, specifically for IO bound systems.
IO Bound tasks tend to spend more time reading or writing to network, disk or other external stuff. Example is making an api call to some other service or loading a file from disk. Compute intensive tasks are those that spend time completely in the cpu and ram. Example would be summing all the values in an array.
For compute intensive programs, asyncio actually hurts the performance. By getting blocked in long running compute intensive code, it starves the other methods that would otherwise only take a small time for their execution.
However, asyncio is really good for IO bound stuff and allows us to support thousands of read and write operations to the disk or network, without using any threads. That is pretty awesome, to be fair.
import asyncio
from asyncio import Queue, sleep
async def producer(q, n):
for item in range(n):
await q.put(item)
await sleep(1)
print(f"[Produced] {item}")
print("[Producer] Done")
await q.put(None)
async def consumer(q):
while True:
item = await q.get()
if item is None:
break
print(f"[Consumer] {item}")
await sleep(1)
print("[Consumer] Done")
n = 10
q = asyncio.Queue()
loop = asyncio.get_event_loop()
loop.create_task(producer(q, n))
loop.create_task(consu
It gives the following output.
python3.6 async_example.py
[Consumer] 0
[Produced] 0
[Consumer] 1
[Produced] 1
[Consumer] 2
[Produced] 2
[Consumer] 3
[Produced] 3
[Consumer] 4
[Produced] 4
[Consumer] 5
[Produced] 5
[Consumer] 6
[Produced] 6
[Consumer] 7
[Produced] 7
[Consumer] 8
[Produced] 8
[Consumer] 9
[Produced] 9
[Producer] Done
[Consumer] Done
^CTraceback (most recent call last):
File "async_example.py", line 28, in <module>
loop.run_forever()
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 421, in run_forever
self._run_once()
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/asyncio/base_events.py", line 1395, in _run_once
event_list = self._selector.select(timeout)
File "/Library/Frameworks/Python.framework/Versions/3.6/lib/python3.6/selectors.py", line 577, in select
kev_list = self._kqueue.control(None, max_ev, timeout)
KeyboardInterrupt
The only problem with the above code is that you need to hit ctrl+C to stop. There are ways to get around it, but it wasn’t so important for this example.
As you can see, the actual producer and consumer methods in both threading as well as async code is very similar in structure. Almost identical with the additional keywords like async and await.
The keyword ‘async’ tells the python interpreter that this method is actually a coroutine or a generator which will run lazily and return values while yielding the flow control.
Await keyword helps to wait on the called method to complete before returning the control to back to the point, where the keyword was used. It is what converts the method into a generator, as there is yield somewhere down there, in the implementation of await.
The example is simple one to get started with asyncio. Happy coding!
Originally published at http://progarsenal.com on October 19, 2019.