Multithreading / Multiprocessing / GIL

TL;DR

Threading ← concurrency (параллелизм), not synchronously

  • Multithreading - IO bound, network bound, uses GIL so only one thread can be running at a time (waiting for input and output operations to be completed, and not using CPU so much), reading/writing from file, downloading online, network operations
  • Multiprocessing - CPU bound (handling lots of numbers and using CPU)
  • Python: When using threading we don’t run all at the same time, it gives the illusion of running at the same time, (HOW?) because when it comes to the point where it’s just waiting around it’s just going to go ahead and move forward with the script and run other code while the IO operations finish.
  • Python: Multiprocessing is more RAM intensive. This is because Python multiprocessing uses pickle to serializes objects when passing them between processes, requiring each process to create its own copy of the data, adding substantial memory usage, not to mention expensive deserialization.
if io_bound:
    if io_very_slow:
        print("Use Asyncio")
    else:
        print("Use Threads")
else:
    print("Multi Processing")
CPU Bound => Multi Processing

I/O Bound, Fast I/O, Limited Number of Connections => Multi Threading

I/O Bound, Slow I/O, Many connections => Asyncio

Concurrency vs Parallelism

  • Concurrency = executing multiple tasks at the same time, but not necessary simultaneously.
  • In a concurrent system, tasks can start, execute, and complete independently of each other, but their execution may overlap in time. It's more about managing multiple tasks or processes efficiently and making progress on all of them over time.
  • Concurrency = multitasking, multithreading, asynchronous programming.
  • Parallelism = simultaneously executing multiple tasks or processes at the same time.
  • In a parallel system, tasks are executed simultaneously, typically by allocating different resources (such as CPU cores) to different tasks. It's about performing multiple computations simultaneously to improve performance and throughput.
Concurrency
Parallelism
One core, done using context switching, simulation of parallelism.
Many cores
Multiple tasks can run in overlapping periods
When tasks actually run in parallel in multiple CPUs.
Concurrency in Python is implemented via: Threading and coroutines, or async.
For parallelism, Python offers multiprocessing, which launches multiple instances of the Python interpreter, each one running independently on its own hardware thread.
More

Thread and process

Thread
Process
A segment of process, sequence of instructions of process
A program in execution
One process can spawn multiple threads but all of them will be sharing the same memory
Processes are isolated
Lightweight
Not lightweight
Share data with each other, two threads can write to the same memory at the same time
Do not share data/objects

Multiprocessing and multithreading

Multithreading
Multiprocessing
ORIGINAL: Each thread runs parallel to each other.
ORIGINAL: Allows the execution of multiple processes in parallel. Creates entirely separate instances of the Python interpreter, each with its own memory space and its own GIL.
I/O-Bound
CPU bound
Many threads are created of a single process for increasing computing power.
CPUs are added for increasing computing power
Multithreading system executes multiple threads of the same or different processes.
Multiprocessing system allows executing multiple programs and tasks.
If you need data shared among different execution entities. Message passing mechanisms are less fast and flexible than shared memory. Therefore, in some cases, it is better to use threads instead of processes.
Reliability: multiprocess applications are usually more reliable because the crash of a process does not affect the other processes.
threading.Thread or ThreadPoolExecutor https://superfastpython.com/threadpoolexecutor-vs-threads/
  • CPU-bound code will have no performance gain with Python multi-threading because of GIL.
  • A Python process cannot run threads in parallel but it can run them concurrently through context switching during I/O bound operations.
    • This limitation is actually enforced by GIL.
    • The Python Global Interpreter Lock (GIL) prevents threads within the same process to be executed at the same time.
    • With threading, code moves to another part, while IO operations are done separately
      With threading, code moves to another part, while IO operations are done separately

I/O bound and CPU bound

I/O bound
CPU bound
Limited by speed of I/O subsystem
Limited by speed of CPU
Counting the number of lines (processing data from disk)
Performing calculations on a small set of numbers
HTTP requests
Multiplying small matrices
Image resizing (because we recalculate each pixel), and only save result when it’s ready

GIL = Global Interpreter Lock

  • Only one thread can execute Python code at once”
  • GIL is a mutex solution for dealing with shared resources (memory), when two threads try to modify the same resource at the same time. Solution is a global lock on the interpreter when a thread is interacting with the shared resource. Python’s GIL accomplishes this by locking the entire interpreter, meaning that it’s not possible for another thread to step on the current one. When CPython handles memory, it uses the GIL to ensure that it does so safely.

Multithreading in Python

Without concurrency
Without concurrency

ProcessPoolExecutor

Python Coroutines and async

♾️Asynchronous Programming

ThreadPoolExecutor vs Thread

ThreadPoolExecutor
Thread
Heterogeneous tasks (Many)
Homogeneous tasks (One)
Reuse threads, not single use
Single-use threads, not multi-use threads
Support for task results, not fire-and-forget.
No support for task results.
Check status of tasks, not opaque.
No support for checking status.

Mutex, race condition

  • Threading Mutex Lock
  • When writing concurrent programs we may need to share data or resources between threads, which typically must be protected with a mutual exclusion lock.
  • Mutex lock is a synchronization primitive intended to prevent a race condition. GIL is also preventing race condition. We won’t have deadlock because GIL uses 1 thread at a time.
  • Race condition — concurrency failure case when two threads run the same code and access or update the same resource (e.g. data variables, stream, etc.) leaving the resource in an unknown and inconsistent state. Result depends on order of execution. Data may lose consistency.
  • Deadlock is when two (or more) threads are blocking each other.

References

SuperMade with Super