Sobes.tech
Middle

What is the difference between asynchronous programming, threads, and multiprocessing?

sobes.tech AI

Answer from AI

Aspect Asynchronous Programming Threads Multiprocessing
Parallelism Pseudo-parallelism (Concurrent) on a single core Pseudo-parallelism (Concurrent) on a single core True parallelism (Parallel) on different cores
Implementation Single-threaded, managed by event loop Threads within a single process Separate processes
Data exchange Shared variables with caution (single process) Shared variables, locks (GIL in Python) Channels, queues, shared memory (less trivial due to separate address spaces)
Context switching Fast, at the level of coroutines/functions Fast, at the OS kernel level Slower, at the OS process level
CPU utilization Idle during I/O wait Blocked during I/O wait Fully utilizes available CPU cores
Best scenarios I/O-bound tasks (network, files) Tasks where only one thread blocks (even with GIL) Compute-intensive tasks
Example Waiting for server response Background file saving Parallel processing of large data sets

Asynchronous programming in Python is based on asyncio and allows efficient management of multiple I/O operations within a single thread. Instead of blocking, a function "suspends" and hands over control to the event loop, which can perform other tasks until the operation completes.

Threads operate within a single process and share the same memory space. However, in CPython, there is the Global Interpreter Lock (GIL), which effectively prevents multiple threads from executing Python code simultaneously on different CPU cores. Therefore, threads are more useful for I/O-bound tasks, where GIL is released during waiting. For CPU-bound tasks, GIL becomes a bottleneck.

Multiprocessing launches independent processes with their own memory spaces. Each process has its own Python interpreter and GIL. This allows achieving true parallelism by distributing compute-intensive tasks across available CPU cores. Data exchange between processes requires explicit mechanisms like queues or channels, as they do not share memory directly.

# Example of asynchronous code
import asyncio

async def task1():
    print("Start task 1")
    await asyncio.sleep(1) # Simulate async operation
    print("End task 1")

async def task2():
    print("Start task 2")
    await asyncio.sleep(2) # Simulate async operation
    print("End task 2")

async def main():
    await asyncio.gather(task1(), task2())

# Run the event loop
if __name__ == "__main__":
    asyncio.run(main())
# Example of using threads
import threading
import time

def task1():
    print("Start thread 1")
    time.sleep(1) # Can block the main thread (from GIL perspective)
    print("End thread 1")

def task2():
    print("Start thread 2")
    time.sleep(2) # Can block the main thread (from GIL perspective)
    print("End thread 2")

# Creating and starting threads
thread1 = threading.Thread(target=task1)
thread2 = threading.Thread(target=task2)

thread1.start()
thread2.start()

# Waiting for threads to finish
thread1.join()
thread2.join()

print("All threads completed")
# Example of using multiprocessing
import multiprocessing
import time

def task1():
    print(f"Start process 1 (PID: {multiprocessing.current_process().pid})")
    time.sleep(1) # Runs independently of others
    print(f"End process 1 (PID: {multiprocessing.current_process().pid})")

def task2():
    print(f"Start process 2 (PID: {multiprocessing.current_process().pid})")
    time.sleep(2) # Runs independently of others
    print(f"End process 2 (PID: {multiprocessing.current_process().pid})")

# Creating and starting processes
if __name__ == "__main__":
    process1 = multiprocessing.Process(target=task1)
    process2 = multiprocessing.Process(target=task2)

    process1.start()
    process2.start()

    # Waiting for processes to finish
    process1.join()
    process2.join()

    print("All processes completed")