Building a petrol station simulator can be an exciting project that involves managing multiple cars, queues, and pumps efficiently. Initially, you might consider creating a separate thread for each car to simulate random arrival times at the station. However, this approach can be resource-intensive and unnecessarily complex. In this post, we’ll explore a more streamlined method using an enumerator to control the arrival of cars in a single thread, while the pumps operate in their own threads to handle fueling.
Initial Approach: One Thread Per Car
Let’s start by looking at the initial implementation where each car is managed by its own thread:
def cars_threads
cars.map do |car|
Thread.new do
Timer.instance.pause_until(rand(cars_delay_range))
queue.push(car)
end
end
end
In this setup:
- Each car is represented by a separate thread.
- Each thread simulates a random arrival delay before pushing the car to the queue.
Issue with the Initial Approach
Resource Overhead: Creating a thread for each car can quickly consume system resources, especially with a large number of cars. Actually it could work with up to 2000 of cars on my M1 Mac. Which cannot simulate a city or even a town. The program was simply getting stuck few steps after the beginning.
Improved Approach: Using a Single Spawner Thread with an Enumerator
To address these issues, we can refactor the code to use a single thread that pushes cars to the queue at random intervals. This is achieved with an enumerator, which generates car arrivals as needed.
Step 1: Define the Enumerator
We create an enumerator that yields cars at random intervals:
def random_interval_enumerator
Enumerator.new do |enum|
cars.each do |car|
break if station.done? # it's closed and pumps finished work
delay = SecureRandom.random_number(cars_delay_interval_range)
Timer.instance.pause_for(delay)
enum.yield(car)
end
end
end
In this method:
- The enumerator iterates over each car.
- For each car, it introduces a random delay before yielding the car.
- The enumerator yields cars only when needed.
Step 2: Create the Spawner Thread
We then create a single thread to push cars to the queue:
def spawner_thread
Thread.new do
random_interval_enumerator.each do |car|
queue.push(car)
end
end
end
In this method:
- A single thread runs the enumerator.
- It pushes each car to the queue at random intervals.
Benefits of the New Approach
Reduced Overhead: Using a single thread for car arrivals reduces the overhead of managing multiple threads. It works now with even 100,000 cars, hundreds of pumps, and can do more. Ruby is not choking basically.
Conclusion
By refactoring our petrol station simulator to use a single spawner thread with the enumerator, we can significantly improve efficiency and simplicity. This approach reduces resource overhead and scales better as the number of cars increases. Meanwhile, each pump operates in its own thread, independently handling the fueling process.
This may sound so obvious! More threads, more resources. But as you model a behaviour of things you may go deep into a rabbit hole of “everything needs is own thread to simulate real behaviour”. Sometimes it doesn’t. The idea of “one car one thread” was in this project from the beginning, now it’s time for change. Let’s see where’s the limit now. Will I need to use Async and Fibers?
If you’re working on a similar simulator or any project involving concurrent tasks, consider leveraging enumerators and simplifying your thread management. This will lead to cleaner, more efficient, and scalable code. Happy simulating!