C# — Parallel Programming: Producer/Consumer Pattern

Saurabh Singh
6 min readOct 10, 2018

Do you guys remember when you started coding? For me, it is more than 15 years, when I wrote my first program while doing my graduation. At that time Intel Pentium III was very popular, ever since then, so many new processors and generations are launched.

But a good developer must know how to exploit the full potential of multiple processor systems.

For example:-

- Host system has an octa-core processor

- Processing 100 records in a loop

- Each iteration takes 3 seconds to complete

A simple For/While loop running in a single thread may take around 300 seconds.

But if your code uses all available processors available, the same loop may complete all 100 iterations in about 30–35 seconds.

.Net framework provides several classes and libraries that provide us with high-level abstraction to control multiple parallel threads. This is my first article in this series of parallel programming, here I will explain Producer-Consumer design pattern to implement parallel processing.

I will explain it with the help of a problem to solve.

Problem:

We have an excel sheet, we import the sheet in a list. We process records that take 2 seconds per iteration.

After processing, we write the record in another excel sheet, that again takes 1 second per record.

So if we have 100 records in input excel then it takes 300 seconds to generate output.

Producer-Consumer Pattern Solution

The first step is to understand the workflow and identify independent operations.

Program workflow

In this case, we can observe that ‘Write Output’ in the output file is independent of processing of ‘Process Records’.

Although Step 3 depends on the output of Step 2, as soon as any record is processed in Step 1, we can write that record to the output file.

When you go to a restaurant and you order a meal. The cook prepares and serve appetizer in the beginning. While you enjoy your appetizer, the cook prepares the main course for you in parallel. Once he serves the main course, he starts preparing desserts while you have your main course.

You cannot eat unless the cook prepares meals for you, but still, you both perform your activities in parallel.

Going back to our problem we know, we know the Step 2 is our producer preparing meals and Step 3 is consumer enjoying the delicious meals.

Basic workflow of Producer-Consumer

Performance Test 1 — Single Thread 300 seconds

Without this pattern in a sequential single thread, the code takes 300 seconds to process 100 records.

200 seconds in Step2 and 100 seconds in Step 3.

Note: My thread is not sleeping, but it is active in while loop to keep CPU/processor busy.

Step 2 — Processing 2 second per record

Step 2

Step 3 — Writing to output file at 1 second per record

Step 3

Performance Test 2 — Two Thread (Producer/Consumer) 200 seconds

Now let us try to implement the pattern and execute both loops simultaneously in parallel to each other. Now as soon as any record is processed by Producer thread, the Consumer thread will pick that record and start processing.

Now Producer is taking 200 seconds to process 100 records and my Consumer processing time is 100 seconds.

Consumer

Single Thread Consumer

Producer

Single Thread Producer

Waiting for both threads to complete

Performance Test 3 — Producer with Parallel.For() 100 seconds

In this test, I replaced the for loop in Producer with Parallel.For(), that starts separate threads for each iteration. It manages the count, generation and disposal of threads itself, so we don’t have to worry about that.

Although there are various advanced configurations available that one can do, you will find more details in the following link.

https://docs.microsoft.com/en-us/dotnet/standard/parallel-programming/how-to-write-a-simple-parallel-for-loop

In this test, the Producer takes very less time to execute which varies depending on host system hardware and the number of physical and virtual processors. It can finish processing in 20 seconds or 30 seconds or 50 seconds etc., depending on the number of processors available to process records in parallel in separate threads inside Parallel.For loop.

Although now Producer is generating records much faster than Consumer, the Consumer is still running on a single thread, it will take 100 seconds to complete. So overall processing time is 100 seconds.

Modified Producer with Parallel.For loop

Producer with Parallel.For loop

Performance Test 4— Consumer with Parallel processing 10-20–40 seconds

We can further improve the performance by writing records to the output file in parallel. But I will leave that part for you try yourself.

When both Producer and Consumer execute in parallel in the thread-safe environment, the code will utilize the full potential of processing power available to it. The execution time will depend on the host machine processing power.

Blocking Collection

Note: There is a reason why we used Blocking Collection instead of normal collection or concurrent bag. They are not thread-safe.

I will leave that for you to read and I may write another article on thread safety. Thread safety in itself is very vast and big topic. Meanwhile, you can read from the following link.

https://docs.microsoft.com/en-us/dotnet/standard/collections/thread-safe/blockingcollection-overview

Conclusion

Without utilizing the full potential of available processing power, the server application’s response time will be the same on a 10-year-old system and the systems available to us in the year 2018.

I explained just a small and basic aspect of parallel programming. Please keep a check of my blogs for articles in this series.

Cheers.

More Readings

--

--

Saurabh Singh

ReactJS, .Net Core, AWS, SQL, Docker, Mongo, RabbitMQ