Last Updated on September 12, 2022
Concurrency programming provides new terminology such as blocking call, sleep, and wait.
These terms have specific meaning in terms of how the operating system treats the threads that are blocking or sleeping.
In this tutorial you will discover what a blocking function call is and the difference between block, sleep, and wait.
Let’s get started.
What is a Block vs Sleep vs Wait
In concurrent programming you often see function calls that are described as blocking or blocking calls, and others described as non-blocking calls.
For example:
1 2 3 |
... # this is a blocking call ... |
And:
1 2 3 |
... # this does not block ... |
You may also see the use of the time.sleep() function, that is used to block.
For example:
1 2 3 |
... # block for a while time.sleep(10) |
Finally, you may see function calls described as blocking that call a wait() function.
For example:
1 2 3 |
... # blocking call condition.wait() |
What is a blocking call and what is the difference between blocking, sleeping, and waiting?
Run loops using all CPUs, download your FREE book to learn how.
What is a Blocking Function Call?
A blocking call is a function call that does not return until it is complete.
All normal functions are blocking calls. No big deal.
In concurrent programming, blocking calls have special meaning.
Blocking calls are calls to functions that will wait for a specific condition and signal to the operating system that nothing interesting is going on while the thread is waiting.
The operating system may notice that a thread is making a blocking function call and decide to context switch to another thread.
You may recall that the operating system manages what threads should run and when to run them. It achieves this using a type of multitasking where a running thread is suspended and suspended thread is restored and continues running. This suspending and restoring of threads is called a context switch.
The operating system prefers to context switch away from blocked threads, allowing non-blocked threads to run.
This means if a thread makes a blocking function call, a call that waits, then it is likely to signal that the thread can be suspended and allow other threads to run.
Similarly, many function calls that we may traditionally think block may have non-blocking versions in modern non-blocking concurrency APIs, like asyncio.
Now that we are familiar with a blocking call, let’s take a look at some examples.
Examples of Blocking Calls
There are many examples of blocking function calls in concurrent programming.
Common examples include:
Waiting for a Lock
Acquiring a mutual exclusion (mutex) lock via threading.Lock is a blocking call.
This is achieved with a call to acquire() that will return immediately (not block) if the lock is available, otherwise block until it is available.
1 2 3 |
... # block until lock is available lock.acquire() |
This can also be achieved more cleanly using a context manager, which too may block if the lock is not available.
1 2 3 4 |
... # block until lock is available with lock: #... |
Waiting to Be Notified
A thread may wait to be notified of some change in program state.
This is often achieved using the wait() function on a threading.Condition instance that will not return until another thread calls notify() or notify_all().
For example:
1 2 3 |
... # block until notified condition.wait() |
More correctly, a thread cannot wait on a condition unless it has acquired the condition first.
A condition can be acquired like a lock by calling the acquire() function or by using the context manager.
Like a lock, acquiring the condition may block if the condition has already been acquired by another thread, otherwise it will not block and return immediately if it has not already been acquired.
For example:
1 2 3 4 5 |
... # block to acquire the condition with condition: # block until notified condition.wait() |
In this example, the calling thread will have two opportunities to be blocked.
Wait for a Thread to Terminate
One thread can block when waiting for another thread to terminate.
This is achieved by the waiting thread calling the join() function on the other running thread.
This function call will block until the other thread finishes, or returns immediately if the thread has already terminated.
1 2 3 |
... # block until the thread finishes thread.join() |
Waiting for a Semaphore
A threading.Semaphore provides thread-safe counter to a predefined limit.
A thread must acquire position on the semaphore using the acquire() function. Once the semaphore is full, additional attempts to acquire it must be blocked until a position becomes available.
This means that like a lock, a call to acquire on a semaphore may or may not block.
For example:
1 2 3 |
... # acquire a position on the semaphore, may block semaphore.acquire() |
Also like a lock, a semaphore may be used via the context manager, which in turn will call acquire() automatically and may block.
For example:
1 2 3 4 |
... # block waiting for a position on the semaphore with semaphore: # ... |
Wait for an Event
A threading.Event is a thread-safe boolean flag.
A thread may block waiting for an event to be set via the wait() function.
For example:
1 2 3 |
... # block waiting for flag to be set event.wait() |
Wait on a Barrier
A threading.Barrier is a synchronization primitive that allows multiple threads to coordinate.
Threads will block on the barrier until a fixed number of parties have arrived, after which they will all be released.
This is achieved by calling the wait() function on the barrier.
For example:
1 2 3 |
... # block on the barrier barrier.wait() |
Free Python Threading Course
Download your FREE threading PDF cheat sheet and get BONUS access to my free 7-day crash course on the threading API.
Discover how to use the Python threading module including how to create and start new threads and how to use a mutex locks and semaphores
Blocking I/O
Conventionally, function calls that interact with IO are often blocking function calls.
That is, they are blocking in the same sense as blocking calls in the concurrency primitives.
The wait for the IO device to respond is another signal to the operating system that the thread can be context switched.
Common examples include:
- Hard disk drive: Reading, writing, appending, renaming, deleting, etc. files.
- Peripherals: mouse, keyboard, screen, printer, serial, camera, etc.
- Internet: Downloading and uploading files, getting a webpage, querying RSS, etc.
- Database: Select, update, delete, etc. SQL queries.
- Email: Send mail, receive mail, query inbox, etc.
And many more examples, mostly related to sockets.
Performing IO with devices is typically very slow compared to the CPU.
The CPU can perform orders of magnitude more instructions compared to reading or writing some bytes to a file or socket.
The IO with devices is coordinated by the operating system and the device. This means the operating system can gather or send some bytes from or to the device then context switch back to the blocking thread when needed, allowing the function call to progress.
As such, you will often see comments about blocking calls when working with IO.
For example, file IO is blocking:
1 2 3 4 |
... # block while reading the file with open(filepath) as file: return file.read() |
Overwhelmed by the python concurrency APIs?
Find relief, download my FREE Python Concurrency Mind Maps
What is Sleep?
The sleep() function is a capability provided by the underlying operating system that we can make use of within our program.
It is a blocking function call that pauses the thread to block for a fixed time in seconds.
This can be achieved via a call to time.sleep().
Suspend execution of the calling thread for the given number of seconds.
— time — Time access and conversions
For example:
1 2 3 |
... # sleep for 5 seconds time.sleep(5) |
It is a blocking call in the same sense as concurrency primitives and blocking IO function calls. It signals to the operating system that the thread is waiting and is a good candidate for a context switch.
Sleeps are often used when timing is important in an application.
In concurrent programming, adding a sleep can be a useful way to simulate computational effort by a thread for a fixed interval.
We often use sleeps in worked examples when demonstrating concurrency programming, but adding sleeps to code can also aid in unit testing and debugging concurrency failure conditions, such as race conditions by forcing mistiming of events within a dynamic application.
What is a Wait?
Wait is the action performed by a thread when blocked.
Often with concurrency primitives, the function call itself may have the name wait() or await(), signaling that the thread will block until a condition is met.
Under the covers, the wait may be implemented in a number of ways, such as calling a wait or sleep system call in the operating system or perhaps using a busy wait.
Further Reading
This section provides additional resources that you may find helpful.
Python Threading Books
- Python Threading Jump-Start, Jason Brownlee (my book!)
- Threading API Interview Questions
- Threading Module API Cheat Sheet
I also recommend specific chapters in the following books:
- Python Cookbook, David Beazley and Brian Jones, 2013.
- See: Chapter 12: Concurrency
- Effective Python, Brett Slatkin, 2019.
- See: Chapter 7: Concurrency and Parallelism
- Python in a Nutshell, Alex Martelli, et al., 2017.
- See: Chapter: 14: Threads and Processes
Guides
- Python Threading: The Complete Guide
- Python ThreadPoolExecutor: The Complete Guide
- Python ThreadPool: The Complete Guide
APIs
References
Takeaways
You now know the difference between block, sleep, and wait.
Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.
Photo by Harley-Davidson on Unsplash
Do you have any questions?