In this article, I explain what queues are and how to use Queues in C#. Have you ever waited in line to buy tickets for a popular movie or event? If so, you have experienced a real-life example of a queue. The first person in line is the first to buy a ticket, and as people join the line, they do so at the end. When someone buys a ticket and leaves the line, the next person in line becomes the first in line and can buy a ticket. This is an example of the first in, first out (FIFO) principle that a queue follows. You can also learn about binary trees in this article.
Creating a Queue
C# provides a built-in class named Queue that implements this data structure. To create a queue in C#, you need to instantiate the Queue class, as shown below:
Queue<string> myQueue = new Queue<string>();
This creates an empty queue that can store elements of string type. You can also initialize a queue with some elements:
Queue<int> myQueue = new Queue<int>(new int[] { 1, 2, 3, 4 });
This creates a queue of integers with four elements: 1, 2, 3, and 4.
There’s also a non-generic implementation of queue built into C#:
Queue myQueue = new Queue(new object[] { 1, 2, 3, 4 });
The non-generic implementation works with objects.
Adding Elements to a Queue
To add an element to the end of the queue, you can use the Enqueue method:
myQueue.Enqueue("element");
This adds the string “element” to the end of the queue.
Removing Elements from a Queue
To remove the element at the beginning of the queue, you can use the Dequeue method:
string element = myQueue.Dequeue();
This removes the first element from the queue and returns it. If the queue is empty, it’ll throw an InvalidOperationException
.
Putting it Together
Let’s simulate the tickets queue example from the intro using code.
Queue<string> ticketsQueue = new Queue<string>();
ticketsQueue.Enqueue("Jannick");
ticketsQueue.Enqueue("Mohsen");
ticketsQueue.Enqueue("Jafar");
ticketsQueue.Enqueue("Jafar");
ticketsQueue.Enqueue("Denis");
string who = ticketsQueue.Dequeue();
Console.WriteLine(who);
And the result is:
Jannick
Let’s move on.
Retrieving the Element at the Beginning of the Queue
To retrieve the element at the beginning of the queue without removing it, you can use the Peek method, as shown below:
string element = myQueue.Peek();
This returns the first element of the queue without removing it. If the queue is empty, it throws an InvalidOperationException
.
Checking if a Queue Contains an Element
To check if a queue contains a specific element, you can use the Contains method, as shown below:
bool containsElement = myQueue.Contains("element");
This returns a boolean value indicating whether the queue contains the specified element. You know how it works now. Let’s see how you can use queues in your real-world apps.
Example: Message Queues
There are times when different system components need to exchange messages without coupling to each other. In these scenarios, a queue can be used to facilitate this exchange by acting as a buffer between the components. Messages can be added to the queue by one component and retrieved by another, allowing the components to communicate without being aware of each other’s internal workings. This can make your code more modular and easier to maintain.
Here’s an example of a simple messaging system that uses a queue in C#:
record Message(string Sender,string Recipient,string Body);
class MessagingSystem
{
private Queue messageQueue = new Queue();
public void SendMessage(Message message)
=> messageQueue.Enqueue(message);
public Message? ReceiveMessage()
=> messageQueue.Count > 0
?(Message?)messageQueue.Dequeue()
:null;
}
Let’s use our messaging system before discussing how it works.
MessagingSystem messagingSystem = new MessagingSystem();
messagingSystem.SendMessage(new Message (Sender: "Component 1", Body: "First Task" ));
messagingSystem.SendMessage(new Message (Sender: "Component 2", Body : "Second Task" ));
Message receivedMessage1 = messagingSystem.ReceiveMessage();
Console.WriteLine($"Received message from {receivedMessage1.Sender}: {receivedMessage1.Body}");
//Prints: Received message from Component 1: First Task
Message receivedMessage2 = messagingSystem.ReceiveMessage();
Console.WriteLine($"Received message from {receivedMessage2.Sender}: {receivedMessage2.Body}");
//Prints: Received message from Component 2: Second Task
Message receivedMessage3 = messagingSystem.ReceiveMessage();
//The message is null
In this example, the MessagingSystem
class has a private messageQueue
field that is used to store messages sent by users. The SendMessage
method adds messages to the end of the queue using the Enqueue
method. The ReceiveMessage
method removes the first message from the queue using the Dequeue
method and returns it. If the queue is empty, it prints a message to the console and returns null.
The Message
class is a simple class that represents a message sent by a user. It has three properties: Sender
, Recipient
, and Body
.
In the example usage code, we create a new MessagingSystem
object and send two messages using the SendMessage
method. We then receive the messages using the ReceiveMessage
method and print their contents to the console. Finally, we try to receive another message when the queue is empty, which prints a message to the console.
Notice that we used the non-generic Queue class. You can also validate the subscriber in the ReceiveMessage
method. Now imagine a proxy that decorates the queue class and transmits the queued items on TCP, and retrieves messages from the network before queueing them. By the way, did you know that we offer a unique online course that boosts your C# career? Check it out here!
Asynchronous Logging
Queues can also be used to implement a logging system in some scenarios. For example, you could use a queue to buffer log messages before writing them to a file or database. This can help improve performance by reducing the number of writes to the file or database and can also make the logging system more resilient to failures.
Here’s an example of how you could use a queue to implement a simple logging system in C#:
class Logger
{
record LogMessage(DateTime Timestamp, string Message);
private Queue logQueue = new Queue();
private string logFilePath;
public Logger(string logFilePath)
=> this.logFilePath = logFilePath;
public void Log(string message)
=> logQueue.Enqueue(new LogMessage(DateTime.Now, message));
public void Flush()
{
using StreamWriter writer = new StreamWriter(logFilePath, true);
while (logQueue.Count > 0)
{
LogMessage logMessage = (LogMessage)logQueue.Dequeue()!;
writer.WriteLine("[{0}] {1}", logMessage.Timestamp, logMessage.Message);
}
}
}
Let’s also use our logger before discussing the mechanics.
Logger logger = new Logger("log.txt");
logger.Log("This is a log message");
logger.Log("This is another log message");
logger.Flush();
As the result, the logger has written the logs in the <app-path>/log.txt
file:
[3/8/2023 2:31:02 AM] This is a log message
[3/8/2023 2:31:02 AM] This is another log message
In this example, the Logger
class has a private logQueue
field used to store log messages. The Log
method adds messages to the end of the queue using the Enqueue
method. The Flush
method writes all the messages in the queue to a log file specified by the logFilePath
parameter. It does this by opening the log file in append mode using a StreamWriter
object and then writing each log message to a new line in the file using the WriteLine
method.
The LogMessage
class is a simple class that represents a log message. It has two properties: Timestamp
, which is a DateTime
object representing the time the message was logged, and Message
, which is a string containing the log message.
In the example usage code, we create a new Logger
object and log two messages using the Log
method. We then flush the messages to the log file using the Flush
method. You can also read more about stacks in this article.
Queues Use Cases
Queues are useful for scenarios where you need to process items in the order they were added. Some common use cases for queues include:
- Message queues: Queues can be used to facilitate communication between different components of a system. For example, one component could add messages to a queue, and another component could read messages from the queue and process them.
- Job queues: Queues can be used to manage a list of tasks that need to be executed. For example, a web server might use a queue to manage incoming requests, processing each request in the order it was received.
- Event queues: Queues can be used to manage a list of events that need to be processed. For example, a game might use a queue to manage user input, processing each input event in the order it was received.
- Cache Queues: Queues can also be used to implement a cache. A cache is a temporary storage area used to store frequently accessed data. By using a queue to manage the cache, you can ensure that the most recently accessed items are kept in the cache while older items are removed.
- Thread Pools: A thread pool is a collection of worker threads that can be used to execute tasks in parallel. By using a queue to manage the tasks, you can ensure that they are executed in the order they were received and that no more than a certain number of tasks are executed concurrently.
Queues are a useful data structure that can simplify many programming tasks. They are commonly used in scenarios where items need to be processed in the order they were added, such as message queues, job queues, and event queues. Queues can also be used to implement a cache, a thread pool, and other useful features. If you want to skyrocket your C# career, check out our powerful ASP.NET full-stack web development course that also covers test-driven development.
Conclusion
Queues are a useful data structure that can simplify many programming tasks. C# provides a built-in Queue class that makes it easy to create and manipulate queues in your code. By using the methods provided by the Queue class, you can perform operations such as adding and removing elements, retrieving the element at the beginning of the queue and checking if a queue contains a specific element.