Skip to content

Instantly share code, notes, and snippets.

@ipwnponies
Last active March 6, 2024 21:35
Show Gist options
  • Save ipwnponies/67082b5080cb3f951d07708d33200ad7 to your computer and use it in GitHub Desktop.
Save ipwnponies/67082b5080cb3f951d07708d33200ad7 to your computer and use it in GitHub Desktop.
concurrent.futures (threading) vs. asyncio (event loop)

Summary

This gist demonstrates the difference between threading and asyncio. To be clear, they're both limited by the Global Interpreter Lock and are both single process, multi-threaded. They are both forms of concurrency but not parallelism.

Threading, via concurrent.futures

Threading (via Thread, concurrent.futures) employs time-slicing of CPU. All threads are given a slot of CPU time to do work. If the thread is blocking (sleeping or blocked on sockets), then off it goes to the next thread. With many threads that are blocked for long periods, this begins to degrade into polling (polling vs. interrupt)

asyncio

asyncio uses an event loop and is more akin to a pub-sub, push notification model. Threads will announce they're blocked by using asyncio methods. The next available thread at the top of the queue is then processed on, until it completes or is blocked again. This has reduced concurrency and can allow one thread to starve out the others. If the access pattern is threads that are blocked for long time, this model will ensure that you don't bother checking a thread, you wait for it to announce it's available.

from concurrent.futures import ThreadPoolExecutor
from time import sleep
def return_after_5_secs(message, time=2):
print('Stariting {}'.format(message))
sleep(time)
print('Fisnihed {}'.format(message))
return message
def concurrent_test():
pool = ThreadPoolExecutor(3)
¦
future = pool.submit(return_after_5_secs, "hello", 5)
future1 = pool.submit(return_after_5_secs, "world", 2)
sleep(5)
print(future.result())
print(future1.result())
# Comment out asyncio import for testing threads, it will mess with threading
import asyncio
import datetime
import random
async def display_date(num, time):
print("Starting Loop: {} Time: {}".format(num, datetime.datetime.now().time()))
# time = random.randint(3, 5)
print('PRocessing.... taking {} seconds'.format(time))
sleep(2)
print('Took 2 seconds of processing, now blocking on io {}'.format(datetime.datetime.now().time()))
await asyncio.sleep(time)
print('Finsihed loop {loop} {time}'.format(time=datetime.datetime.now().time(), loop=num))
# await my_sleep_func(time)
def asyncio_test():
loop = asyncio.get_event_loop()
asyncio.ensure_future(display_date(1, 10))
asyncio.ensure_future(display_date(2, 5))
loop.run_forever()
if __name__ == '__main__':
# concurrent_test()
asyncio_test()
@ipwnponies
Copy link
Author

@DuncanNguyez one is multi-threading and the other is event-driven paradigm. These are abstract computer science concepts, not able to be explained within this gist.

If forced to give a blanket recommendation, you'd be safest with asyncio. Because the GIL in python (to be removed soon™) reduces multi-threading to single process: it effectively behaves like asyncio but with complete lack of control over control flow. It's unpredictable when context switching will happen, so you expose yourself to all the gotchas with concurrency issues (e. g.race conditions, clobbering memory).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment