Concurrency and Parallelism
Concurrency and Parallelism
Parallelism
Understanding Multitasking in Python
What is Multitasking?
• Multitasking = doing multiple tasks at once
•Concurrency:
•Multiple tasks progressing at the same time
•Can happen on a single CPU (context switching)
•Parallelism:
•Multiple tasks running at the same time
•Requires multiple CPUs/cores
•Analogy:
•Concurrency = Single chef cooking many dishes
•Parallelism = Multiple chefs cooking in parallel
Key Differences
Concurrency with Threading
• The threading module allows multiple threads to run concurrently within
the same process.
• Threads share the same memory space, making communication easier but
requiring synchronization.
• The Global Interpreter Lock (GIL) prevents multiple threads from executing
Python bytecode simultaneously, limiting true parallel execution for CPU-
bound tasks.
• Best suited for I/O-bound tasks like web requests, file I/O, or database
queries.
Python and the Global Interpreter
Lock (GIL)
• Python uses the GIL
import threading
lock = threading.Lock()
Lock
• Once you have created a lock instance, you can use it to protect a shared
resource.
• To acquire the lock, you use the acquire() method
lock.acquire()
• This will block the thread until the lock becomes available.
• Once the lock is acquired, the thread can access the shared resource
safely.
• When the thread is done accessing the resource, it must release the lock
using the release() method
lock.release()
Important note
• It’s important to note that when using locks, you must ensure that you release the lock in
all possible code paths.
• If you acquire a lock and then exit the function without releasing the lock, the lock will
remain locked, preventing other threads from accessing the shared resource.
• To avoid this, it’s a good practice to use a try-finally block to ensure that the lock is
released, even if an exception occurs:
lock.acquire()
try:
# access the shared resource
finally:
lock.release()
Using Lock in Python
• Lock ensures only one thread accesses a resource at a time
import threading
lock = threading.Lock()
def safe_increment():
with lock:
# critical section
print("Locked section")
t1 = threading.Thread(target=safe_increment)
t2 = threading.Thread(target=safe_increment)
t1.start()
t2.start()
Use with lock: for auto-release
RLock – Re entrant Lock
• RLock allows the same thread to acquire a lock multiple times
• Use case: Recursive functions with locks
import threading
lock = threading.Lock()
lock = threading.RLock()
# semaphore to limit the number of threads that can access the list simultaneously to 4
semaphore = threading.Semaphore(value=4)
def process_item(item):
semaphore.acquire() # acquire the semaphore
try:
sleep(3) # simulate some processing time
print(f'Processing item {item}') # process the item
finally: # Make sure we always release the semaphore
semaphore.release() # release the semaphore
# semaphore to limit the number of threads that can access the list simultaneously to 4
semaphore = threading.Semaphore(value=4)
def process_item(item):
with semaphore: # acquire the semaphore
sleep(3) # simulate some processing time
print(f'Processing item {item}') # process the item
• A CPU-bound task spends most of its time doing heavy calculations with
the CPUs.
• In this case, you should use multiprocessing to run your jobs in parallel
and make full use of your CPUs.
• An I/O-bound task spends most of its time waiting for I/O responses,
which can be responses from web pages, databases or disks.
• If you’re developing a web page where a request needs to fetch data
from APIs or databases, it’s an I/O-bound task.
• Concurrency can be achieved for I/O-bound tasks with either asyncio or
threading to minimize the waiting time from external resources.
Concurrency with AsyncIO
• The task of Lock class is to claim lock so that no other process can
execute the similar code until the lock has been released.
• So the task of Lock class is mainly two. One is to claim lock and other
is to release the lock.
• To claim lock the, acquire() function is used and to release lock
release() function is used.
When to Use What?
• Use threading for I/O-bound tasks (e.g., network requests, file I/O)
where tasks spend time waiting.
• Use asyncio when you need to handle many asynchronous tasks
efficiently.
• Use multiprocessing for CPU-bound tasks (e.g., heavy computations,
data processing) where actual parallel execution is required.
• Hybrid Approaches: Sometimes, a mix of these techniques is
beneficial, such as combining threading with async operations.