Dive into Queue Module in Python — It’s more than FIFO | by Xiaoxu Gao | Towards Data Science

Queue is a very practical data structure. It’s normally compared with stack, array, linked list, and other linear data structures. What makes a linear structure differs from another is where additions and removals occur.
Queue data structure has three types: FIFO, LIFO, and Priority Queue. The queue module in Python implements all of these types. Besides, it’s extremely useful in thread programming as well which makes this module even more shining.
In this article, I will first help you refresh knowledge on the data structures and learn how to use module queue. Then I will show you how to use queue to solve 2 different multi-threading problems. In the end, I will talk about the pros and cons of this module.
Before moving to some of the advanced features, let’s talk a bit about the queue data structure and learn how to use the module to create a “simple” queue.
FIFO — First in, First out
This is the basic type of queue. Elements inserted first will be removed first, which works like a waiting list in the real world. The earlier you are added to the list, the earlier you will get the concert ticket.
The Python module provides queue.Queue() and queue.SimpleQueue() that implements a FIFO queue. queue.SimpleQueue() is a new feature in Python 3.7. There are 2 differences between them:
SimpleQueue() doesn’t do task tracking in the thread programming. Thread programming will be discussed later.SimpleQueue() is an unbounded FIFO queue while Queue() can have an upper bound. In both classes, if the queue is empty, get() operation will be blocked until new elements are inserted. In Queue(), if the queue is full, the put() operation will be blocked as well until elements are removed. This will never happen to SimpleQueue(). According to Python doc, it’s possible to disable the block using block=False in both get() and put(), then you will receive a queue.Full and queue.Empty exception immediately.
SimpleQueue() doesn’t do task tracking in the thread programming. Thread programming will be discussed later.
SimpleQueue() is an unbounded FIFO queue while Queue() can have an upper bound. In both classes, if the queue is empty, get() operation will be blocked until new elements are inserted. In Queue(), if the queue is full, the put() operation will be blocked as well until elements are removed. This will never happen to SimpleQueue(). According to Python doc, it’s possible to disable the block using block=False in both get() and put(), then you will receive a queue.Full and queue.Empty exception immediately.
LIFO — Last in, First out
LIFO queue has another name: Stack. Elements inserted last will be removed first. This works like the shelves in the supermarket. People normally take the items on the first row which are added lately.
This is implemented in queue.LifoQueue() class. The interface is the same as queue.Queue() except for the order of removing elements. If you compare the results with the previous ones, you should see the difference in the printing result.
Priority Queue
As the name states, the priority queue gives a priority to each element in the queue. It first pops up the element with the highest priority. In the following graph, element 5 has the highest priority, so it will be the first element popped up in case 2 and 3.
Now the question is how Python decides the priority? In the queue module, the lowest valued entries are retrieved first. The lower the value is, the higher the priority it has. Then in the previous example, 2 actually has a higher priority than 5. This is because internally Python priority queue uses min heap queue algorithm. A property of a min heap is that the smallest element is the first element.
A heap is in fact a binary tree where node has a value less than its children. When a new node is pushed to the tree, the tree will be sorted again. And the root node is always the next element to be retrieved. Time complexity for push() and pop() operation is O(log n) and O(1).
The priority queue doesn’t only work with numbers but also complex data types like tuple or customized classes as long as the objects are comparable. To make a class object comparable, you need to implement a couple of rich comparison methods. A simpler way is to use @dataclass, dataclass can implement these methods for you with config order=True.
In this example, the objects are first sorted by key and then by value.
There are many applications of queue data structure. A well-known use case must be the message queue in which we have a producer and a consumer. The producer and consumer usually sit in different threads. Luckily, Queue() class has a thread-safe implementation with all the required locking mechanism. So producer and consumer from different threads can work with the same queue instance safely and easily.
Essentially, queue stores a number of “tasks” to be done, threads can insert tasks into the queue and take tasks from the queue when they are available, and then complete the task and come back for the next task.
If you are new to the threading in Python, I would highly recommend you to read An Intro to Threading in Python from Real Python. It’s a very nice article.
realpython.com
Ok, I assume you’ve read this article and have a good understanding of how threading module works in Python. Let’s combine it with Queue() class. I gave 2 examples here. They are slightly different use cases.
Example 1: Create a queue of image URLs, and download those images “in parallel ” with multiple threads. (If you don’t understand why I added quotes here, please read the above article)
I will use save_image function in this example.
def save_image(id, url):    with open(f'pic{id}.jpg','wb') as image:        response = requests.get(url, stream=True)        for block in response.iter_content(1024):            if not block:                break            image.write(block)
Each Thread object has start() method that starts up a thread, and also join() method that informs the main thread to wait until the thread x completes running. We can solve the problem like this:
Since Queue() is designed for multi-threads, it also offers 2 methods that support task tracking: Queue.task_done() and Queue.join(). Queue.task_done() is to indicate that a task in the queue has been processed, it’s usually called after get(). Queue.join() is similar to Thread.join() which will block the main thread until all the tasks in the queue have been processed. But you don’t have to create a worker list workers and do a loop in the end to join every thread, but just do a single queue.join(). Let’s check out the code.
Example 2: Create a message queue, a producer and a consumer. Producer and consumer from different threads are able to interact with the same queue instance. The producer will send a sentinel message to tell the consumer to stop the process.
In Example 1, we let 10 different threads execute the same function. But in this example, 2 threads are doing completely different jobs. Since there is no real “parallel processing” in Python threading, one thread will do the job first. In this case, producer thread is triggered first and then the consumer thread. You can also see this behavior in the results.
If I uncomment line 9, what would be the result? Think about it! 😄
If I suspend the producer thread before each put() operation, then we will see the interaction between producer and consumer more clearly.
Insert element 0Retrieve element 0Insert element 1Retrieve element 1Insert element 2Retrieve element 2Insert element 3Retrieve element 3Insert element 4Insert sentinelRetrieve element 4Receive SENTINEL, the consumer will be closed.
An obvious advantage of queue is its thread-safe characteristic. But we can’t just take that convenience for granted. What will happen if we don’t have thread-safe queue? What if I want to share a dictionary or a customized class object among multiple threads?
For beginners, probably they will run into race conditions issues. Again, the same article has a very intuitive example about race condition. Basically, the same value will not be synchronized among threads. A solution is to use threading.Lock(). But it requires some extra code and extra attention to control threads.
Another advantage is to avoid potential memory explosion. If the producer works much faster than the consumer, then tasks will be stored in the “pipeline” until it reaches to the memory boundary. In this case, maxsize attribute can avoid this problem.
queue also helps the program to reduce busy waiting. The program doesn’t need to keep trying to pull something from the “pipeline”, but the thread will be blocked until the data is available.
Meanwhile, we should also understand the limitation of queue. queue works with multi-threads, so the programs will not run faster because of this. Also, maxsize refers to the number of items in the queue, not the memory size. So, it’s still possible to reach the limit.
I hope you enjoyed this article! Leave your comments below if you have any thoughts.