Skip to content

Instantly share code, notes, and snippets.

@pssolanki111
Last active September 27, 2021 12:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pssolanki111/9fe2fce37ca7e48ace3b0a032cb905b3 to your computer and use it in GitHub Desktop.
Save pssolanki111/9fe2fce37ca7e48ace3b0a032cb905b3 to your computer and use it in GitHub Desktop.

alrightttyyy. @Baker XBL and anyone who cares : D

Before i go on to the explanation, keep in mind the difference of IO bound operations, and CPU bound operations. and if possible, recall the short note I commented earlier about which one to use in what situation. It's fine if you don't. You can find that in the very end of this note. But it is nicer to first read the other stuff in sequence.

=================================================

the most basic difference is how they are implemented. threads and processes (multiprocessing) are what we know as concurrent execution. In python, threads are not concurrent due to implementation restrictions (the GIL) but in most languages (even python built on other langs - Jpython or iron python), threads are concurrent. In python, threaded workers run in their own threads but due to the GIL restriction, only one thread can use the interpreter python.exe at a time (which is a fancy way to say: only one thread can be running code at a time).

so even though python threads are not parallel, they are still generally considered to be parallel.

how do threads and processes differ?

threads always share the same data in memory.(what that means is that a thread can access the variables defined in say main thread without having to pass those variables in through a communication method like queues). because threads are controlled by the OS (this concept is a part of Operating Systems and Kernels btw) which makes them nice to use in situations where your threads need to communicate to each other a lot.

processes are perfect for when you have to process loads of data, as parallel processing can help process stuff faster. you can employ many worker processes to do stuff concurrently. Processes are independent of each other, do not share in memory data. This is truly parallel in all languages. This has overhead of spawning a process and it take resources more than threads. So they should only be used when there is lot of processing needed and the overall overhead of spawning and running processes is better than linear processing.

Note that communication between processes is very possible but it may not be easy for everyone. Not talking about writing code that does it. But managing race conditions and deadlocks, synchronizing processes (you can generally consider threads in here too but as cpython already controlls them from being true parallel, those conditions are less probable).

then why use threads at all if they don't run parallel? (only in python - that too cpython - which is the official distro)

first, they are still better than no threading. If a thread is waiting for IO, or event (like a GUI), interpreter can hand the GIL (which is a lock - Global Interpreter Lock) to anther thread and it can run its process - all these things happen within nano seconds to microseconds so we don't notice delays.

second they have less over head of spawning and managing them. They can run on a single CPU core (or more as needed). Notice that you would never need to manage what core a thread runs on - because as stated above, threads are managed by the OS.

lastly, if you were using them in a different language or maybe even a different flavour of python, threads are parallel.

yeah okay, threads and processes are parallel (threads not in py), what the hell is asyncio then?

in simplest words, asyncio is not parallel. It never is. You are almost always sure that at a certain point in time, only one thing could be running.

How it works?

asyncio always runs in a single thread. There are no parallel processes, or threads.

it uses a concept of coroutines (I'll use coro to indicate coroutine below)

what's a coro?

take a requests.get() for example. This is an IO bound operation. (Network IO to be specific). we know this could take time depending on how fast the server and our internet connection is. If you make this request in a python file, you know that it will not execute any line of code after the .get() call until it has received the response.

this process (command or task or whatever the heck you like to call it - I have used the words in non-tech references) is called a coroutine. something which is IO bound and can take a short amount of time (a few second - if more than that, you're better off with threads).

Note that while i explicitely mentioned IO bound above, asyncio can be used with cpu bound tasks which are short and take a few seconds at max. for more demanding cpu tasks, multiprocessing is the way to go. but but...

what exactly is IO bound and CPU bound tasks/operations

IO bound in simplest terms is any task where the system waits for Input/Output from a device/network/disk or whatever the interface may be. a file read/write is IO bound (file IO) a requests.get() call is IO bound (network IO)

the time taken in these operations doesn't primarily depend on the CPU strength. That depends on the devices involved in the interaction. (SSD/HDD in a file IO, internet connection/server speed for a network IO)

CPU bound is a task which doesn't need to wait for any data or signal from a device/network. It's just some processing which has to be done by the CPU (or GPU (or TPU :D ))

take this for example

for x in range(1000000000000000):
    for y in range(132323323333233):
        for p in range(3232323333):
            k += 1

does this need to wait for any input/output operation? No. but will it take time to execute? yes. because the CPU needs to do the processing.

Need something better? think about heavy processing on an option chains dataframe of 20 strikes using pandas? yes that's CPU bound.

soomething as basic as time.sleep(10) is cpu bound.

think Now you know why multiprocessing improves run time for cpu bound tasks. By using more of the available hardware (CPU cores).

will it improve runtime for a program which has IO bound tasks? No. that's where you need threading or asyncio.

getting back to asyncio after this quick desc of different tasks

so a coro is just a task which can take some small amt of time.

how does asyncio handle these coro

it uses something called an event loop. (think about it as an event/contract manager). any coro is a new task (contract/event). and needs to be performed. the main loop can be though of as the main thread or main process.

Now since we know everything is in a single thread (remember asyncio is not concurrent), the event loop constantly keeps switching back between these different tasks. Once a task is complete, it is taken off the tasks list (contract list).

when does it switch? when a coro is waiting for IO (could be disk/network or maybe even a small cpu bound task). this switching interface is very well designed and very robust.

still unsure?

remember how you used await requests.get(). why did you need to await the request? 'cause it's a network IO operation and can take some time. So we make this a coroutine. The asyncio event manager will execute this coro (along with other coros and the main event loop). if any of the coros is waiting for the IO, the execution is instantly switched to anther coro which needs to be run.

what if a coro is loooong

you can always use asyncio.sleep() to make that particular coro sleep for that many seconds which means the other coros can now get the event loop.

if were following from above, i said time.sleep() is cpu bound. cpu bound tasks will block the coroutine until they are finished which means other coro will not get the event loop run until this current coro's cpu bound thingy finishes.

(note that if it's IO bound, asyncio automatically switches back and forth between coros but not if a cpu task is encountered) - this is the reasonw why asyncio is not used with processing heavy applications. it cane be but it shouldn't be. if you really want to asyncio.sleep(0) is what can make things a tiny more nicer. but again just don't.

back to sleep vs sleep, asyncio.sleep() is not cpu bound, that's why it's different. it does not block the current coro execution. instead it lets the event loop know that I don't need the event loop right now as Im sleeping so you can give it to some other coro.

conclusion and Take-away

I'ma be lazy and hence will put the exact text i posted earlier in one of the channels. this is of course not a complete conclusion but should make things clearer or at least nicer to take a note of.

for I/O bound tasks which do not take long, asynchronous processing is the way to go. 
for I/O bound long operations and cpu bound short operations, threading fits better.
for cpu bound long operations, multiprocessing is a good choice.

more reading with code samples and diagrams: Article by real Python

Comment Roger That below if you made it this far. Kidding, you don't have to.

Thanks : )

@MarketMakerLite
Copy link

Roger That

@pssolanki111
Copy link
Author

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