Threading Semaphore in Python - Super Fast Python

You can use a semaphore in Python by threading.Semaphore class.

In this tutorial you will discover how to use a semaphore for concurrent programming.

Let’s get started.

Need for a Semaphore

A thread is a thread of execution in a computer program.

Every Python program has at least one thread of execution called the main thread. Both processes and threads are created and managed by the underlying operating system.

Sometimes we may need to create additional threads in our program in order to execute code concurrently.

Python provides the ability to create and manage new threads via the threading module and the threading.Thread class.

You can learn more about Python threads in the guude:

In concurrent programming, we may need to limit the number of concurrent threads that can perform an action simultaneously, such as execute a critical section, perform a calculation, or operate an external resource.

This could be achieved with a mutual exclusion lock and a counter that would have to be manually maintained.

An alternative is to use a semaphore.

What is a semaphore and how can we use it in Python?

What is a Semaphore

A semaphore is a concurrency primitive that allows a limit on the number of threads that can acquire a lock protecting a critical section.

It is an extension of a mutual exclusion (mutex) lock that adds a count for the number of threads that can acquire the lock before additional threads will block. Once full, new threads can only acquire a position on the semaphore once an existing thread holding the semaphore releases a position.

Internally, the semaphore maintains a counter initialized to a provided value, protected by a mutex lock that is decremented each time the semaphore is acquired and incremented each time it is released.

A semaphore manages an internal counter which is decremented by each acquire() call and incremented by each release() call. The counter can never go below zero; when acquire() finds that it is zero, it blocks, waiting until some other thread calls release().

threading — Thread-based parallelism

When a semaphore is created, the upper limit on the counter is set. If it is set to 1, then the semaphore will operate like a mutex lock.

A semaphore provides a useful concurrency tool for limiting the number of threads that can access a resource concurrently. Some examples include:

  • Limiting concurrent socket connections to a server.
  • Limiting concurrent file operations on a hard drive.
  • Limiting concurrent calculations.

Now that we know what a semaphore is, let’s look at how we might use it in Python.

How to Use a Semaphore

Python provides a semaphore via the threading.Semaphore class.

The threading.Semaphore instance must be configured when it is created to set the limit on the internal counter. This limit will match the number of concurrent threads that can hold the semaphore.

For example, we might want to set it to 100:

...

# create a semaphore with a limit of 100

semaphore = Semaphore(100)

In this implementation, each time the semaphore is acquired, the internal counter is decremented. Each time the semaphore is released, the internal counter is incremented. The semaphore cannot be acquired if the semaphore has no available positions in which case, threads attempting to acquire it must block until a position becomes available.

The semaphore can be acquired by calling the acquire() function, for example:

...

# acquire the semaphore

semaphore.acquire()

By default, it is a blocking call, which means that the calling thread will block until a position becomes available on the semaphore.

The “blocking” argument can be set to False in which case, if a position is not available on the semaphore, the thread will not block and the function will return immediately, returning a False value indicating that the semaphore was not acquired or a True value if a position could be acquired.

...

# acquire the semaphore without blocking

semaphore.acquire(blocking=False)

The “timeout” argument can be set to a number of seconds that the calling thread is willing to wait for a position on the semaphore if one is not available, before giving up. Again, the acquire() function will return a value of True if a position could be acquired or False otherwise.

...

# acquire the semaphore with a timeout

semaphore.acquire(timeout=10)

Once acquired, the semaphore can be released again by calling the release() function.

...

# release the semaphore

semaphore.release()

More than one position can be made available by calling release and setting the “n” argument to an integer number of positions to release on the semaphore.

This might be helpful if it is known a thread has died without correctly releasing the semaphore, or if one thread acquires the same semaphore more than once.

Do not use this argument unless you have a clear use case, it is likely to get you into trouble with a semaphore left in an inconsistent state.

...

# release three positions on the semaphore

semaphore.release(n=3)

Finally, the threading.Semaphore class supports usage via the context manager, which will automatically acquire and release the semaphore for you. As such it is the preferred usage, if appropriate for your program.

For example:

...

# acquire the semaphore

with semaphore:

# ...

It is possible for the release() method to be called more times than the acquire() method, and this will not cause a problem. It is a way of increasing the capacity of the semaphore.

A threading.BoundedSemaphore class can be used which will check to ensure that the internal counter does not go above the initial value via calls to release(). If it does, a ValueError will be raised, treating it as an error condition.

Now that we know how to use the threading.Semaphore in Python, let’s look at a worked example.


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

Learn more
 


Example of Using a Semaphore

We can explore how to use a threading.Semaphore with a worked example.

We will develop an example with a suite of threads but a limit on the number of threads that can perform an action simultaneously. A semaphore will be used to limit the number of concurrent threads which will be less than the total number of threads, allowing some threads to block, wait for a position, then be notified and acquire a position.

First, we can define a target task function that takes the shared semaphore and a unique integer as arguments. The function will attempt to acquire the semaphore, and once a position is acquired it will simulate some processing by generating a random number and blocking for a moment, then report its data and release the semaphore.

The complete task() function is listed below.

# target function

def task(semaphore, number):

    # attempt to acquire the semaphore

    with semaphore:

        # process

        value = random()

        sleep(value)

        # report result

        print(f'Thread {number} got {value}')

The main thread will first create the threading.Semaphore instance and limit the number of concurrent processing threads to 2.

...

# create a semaphore

semaphore = Semaphore(2)

Next, we will create and start 10 threads and pass each the shared semaphore instance and a unique integer to identify the thread.

...

# create a suite of threads

for i in range(10):

    worker = Thread(target=task, args=(semaphore, i))

    worker.start()

The main thread will then wait for all worker threads to complete before exiting.

Tying this together, the complete example of using a semaphore is listed below.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

# SuperFastPython.com

# example of using a semaphore

from time import sleep

from random import random

from threading import Thread

from threading import Semaphore

# target function

def task(semaphore, number):

    # attempt to acquire the semaphore

    with semaphore:

        # process

        value = random()

        sleep(value)

        # report result

        print(f'Thread {number} got {value}')

# create a semaphore

semaphore = Semaphore(2)

# create a suite of threads

for i in range(10):

    worker = Thread(target=task, args=(semaphore, i))

    worker.start()

# wait for all workers to complete...

Running the example first creates the semaphore instance then starts ten worker threads.

All ten threads attempt to acquire the semaphore, but only two threads are granted positions at a time. The threads on the semaphore do their work and release the semaphore when they are done, at random intervals.

Each release of the semaphore (via the context manager) allows another thread to acquire a position and perform its calculation, all the while allowing only two of the threads to be processed at any one time, even though all ten threads are executing their run methods.

Note, your specific values will differ given the use of random numbers.

Thread 1 got 0.4468840323081351

Thread 0 got 0.7288038062917327

Thread 2 got 0.4497887327563145

Thread 4 got 0.019601471581036645

Thread 3 got 0.5114751539092154

Thread 6 got 0.6191428550062478

Thread 5 got 0.9893921843198458

Thread 8 got 0.022640379341017924

Thread 7 got 0.20649643092073067

Thread 9 got 0.18636311846540998

More Help With Semaphores

If you need more help with semaphores, some of these tutorials may be interesting:

Semaphores with threads:

Semaphores with processes:

Semaphores with asyncio:


Python Threading Jump-Start

Loving The Tutorials?

Why not take the next step? Get the book.

Learn more
 


Further Reading

This section provides additional resources that you may find helpful.

Python Threading Books

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

APIs

References

Takeaways

You now know how to use a semaphore in Python.

Do you have any questions?
Ask your questions in the comments below and I will do my best to answer.

Photo by NoName on Unsplash