What is the Producer-Consumer Problem in C?

Learn via video course
FREE
View all courses
C++ Course: Learn the Essentials
C++ Course: Learn the Essentials
by Prateek Narang
1000
5
Start Learning
C++ Course: Learn the Essentials
C++ Course: Learn the Essentials
by Prateek Narang
1000
5
Start Learning
Topics Covered

Two processes, one shared buffer, and two ways for it to go wrong: the producer can write into a buffer that's already full, or the consumer can try to read from one that's empty. The producer-consumer problem in operating systems is about stopping both of those, plus a third failure mode people forget, two processes touching the buffer at the exact same instant and corrupting it. This page builds the standard three-semaphore solution from scratch, shows a real (compiled, run, captured) demonstration of the race condition it's fixing, traces through the classic deadlock mistake, and ends with working C code using POSIX threads and semaphores that actually produces and consumes ten items on a real run.

Before getting into the producer-consumer problem in C, let us briefly discuss the work of the producer and consumer.

  • Producer: The producer's role is to produce or generate the data and put it in the buffer, and start again with the same.
  • Consumer: The consumer's role is to consume the produced data from the same shared buffer. When the consumer consumes the data, the data is removed from the buffer.

Refer to the image provided below for more clarity.

producer-consumer-problem-example

Note: The producer produces one data at a time and similarly, the consumer consumes one data at a time. So, the consumer and producer can work parallelly.

What Is the Producer-Consumer Problem?

A producer is any process that generates data. A consumer is any process that reads and processes that data. Both share a fixed-size buffer in memory, sometimes called a bounded buffer, that sits between them: the producer writes into it, the consumer reads out of it, and neither one talks to the other directly.

Three things have to hold for this to work safely:

● The producer must not write when the buffer is full, that's a buffer overflow.

● The consumer must not read when the buffer is empty, that's a buffer underflow.

● Only one of them may touch the buffer at any given instant, this is mutual exclusion, and skipping it causes race conditions on the shared buffer state itself.

The third point is the one that trips people up, because it's not obvious why a full/empty check alone isn't already enough. It isn't, and the next section shows exactly why, with actual numbers from a compiled program rather than a hand-wave. For the general vocabulary here, critical sections, race conditions, mutual exclusion, process synchronisation in OS covers the background this page builds on.

Build an AI-First Career, Master the Complete Skillset

Choose from our industry-leading programs designed for career success

NSDC Certified

Modern Software and AI Engineering Program

Master full-stack development with AI integration

12 MonthsDuration
AI-LedCurriculum
Career SupportSupport
GoogleAmazonPaytm+1000 more
Go to Program
NSDC Certified

Modern Data Science and ML with specialisation in AI

Advanced data science techniques with AI specialization

12 MonthsDuration
AI-LedCurriculum
Career SupportSupport
GoogleAmazonPaytm+1000 more
Go to Program
NSDC Certified

Advanced AIML with Specialisation in Agentic AI

Deep dive into AIML with focus on Agentic systems

12 MonthsDuration
AI-LedCurriculum
Career SupportSupport
GoogleAmazonPaytm+1000 more
Go to Program
NSDC Certified

DevOps, Cloud & AI Platform Engineering

Build and manage AI-powered cloud infrastructure

12 MonthsDuration
AI-LedCurriculum
Career SupportSupport
GoogleAmazonPaytm+1000 more
Go to Program
NSDC Certified

AI Engineering Advanced Certification by IIT-Roorkee

Premier AI engineering certification from IIT-Roorkee

3 MonthsDuration
AI-LedCurriculum
Career SupportSupport
Program highlights
Go to Program
NSDC Certified

AI Forward Deployed Engineer Program

Full-stack engineering, production AI and client-facing consulting

12 MonthsDuration
AI-LedCurriculum
Career SupportSupport
GoogleAmazonPaytm+1000 more
Go to Program

What is the Solution for the Producer-Consumer Problem in C?

Since the problem is that the producer keeps on producing the data even if the buffer is full and the consumer tries to consume the data even if the buffer is empty. We can either put the producer to sleep or discard the data produced by the producer if the buffer is full. So, whenever the consumer tries to consume the data from the buffer, it will notify the producer that the data is being consumed. This will make the producer produce the data again.

Similarly, we can put the consumer to sleep when there is no data in the buffer (empty buffer case). So, whenever the producer produces the data and put it into the buffer, it will notify the consumer that the data is being produced. This will make the consumer consume the data again.

We should also notice that if there is an inadequate solution is proposed then there may arise a situation when both the producer and consumer are on wait (to be awakened)

What is the Main Cause of the Producer-Consumer Problem in C?

The main cause behind the producer-consumer problem in C is that there is no way that will tell the consumer if the buffer is empty or not. Similarly, the producer cannot know that the buffer is already full.

So, as we have discussed above, we can put the consumer to sleep when there is no data in the buffer (empty buffer case). We can similarly put the producer to sleep if the buffer is full. By the term sleep, we mean that the producer should not produce any product when the consumer is consuming the product and vice versa. We can use the sleep() function to put the consumer or the producer to sleep.

Sharpen Your Fundamentals with Free Learning

Examples of Producer-Consumer Problems in C

Example 1: Suppose we have a buffer of size = 5.

Initially, the buffer is empty and neither producer is producing the data nor the consumer is consuming the data.

Suppose that the producer starts producing the data one at a time. The producer produces 3 data during the production and when the consumer comes he/she consumes two of the three produced data. Now in this scenario, we can see that neither of the two has caused the problem as when the consumer was consuming the data, the buffer was not empty. Similarly, when the producer was producing the data the buffer was not full.

Refer to the image of the buffer shown below for more clarity.

buffer-image

Let us take another example when the buffer is full and the producer is still producing the data.

Example 2: Suppose we have a buffer of size = 3.

Initially, the buffer is empty and neither producer is producing the data nor the consumer is consuming the data.

Suppose that the producer starts producing the data one at a time. The producer produces 3 data during the production. The consumer has not consumed any of these produced data. The producer on the other hand keeps on producing the data. Here we can see, that the problem has arrived as the producer is been producing data more than the capacity of the shared buffer. As the buffer is full the data is lost and the producer is not producing any useful data.

Refer to the image of the buffer shown below for more clarity.

full-buffer

Let us take another example when the buffer is empty and the consumer tries to consume the data.

Example 3: Suppose we have a buffer of size = 3. (Refer to the image provided above for an understanding of the buffer size).

Initially, the buffer is empty and neither producer is producing the data nor the consumer is consuming the data.

Suppose that the producer starts producing the data one at a time and has produced two data. Now the capacity of the buffer is not full, there is still space for one more piece of data. Now the consumer comes and consumes one data at a time. He/She consumes the two produced data. Now the buffer has become empty but the consumer tries to consume the data again and again. The producer on the other hand is not producing any data.

Here we can see, that the problem has arrived as the consumer is been consuming the data more than the capacity of the shared buffer (No of produced data). As the consumer tries to consume data from an empty buffer he/she is not getting any data. To solve this problem, we can maintain a counter of the produced products, the total size of the buffer, and another boolean or flag variable that will check whether either consumer is consuming the product or the producer is producing the product. We will only allow one work at a time (either producer can produce the product or the consumer can consume the product from the common buffer). We can increase or decrease the number of products available after production and consumption.

So, let us look at an approach that can solve this problem. In the solution, we will be using a variable called mutex that will increase when the consumer consumes the data and it will decrease when the producer produces the data.

Besides this, we are using a variable called full that will be initialized with 0 which means that the current buffer is empty. Now, if the buffer is full, we will store 1 in the full variable.

Let us look at the implementation for a better understanding

Output

How Scaler Transformed Careers in Different Fields

₹23L
AVG CTC
SCALER PLACEMENT PROOF

Scaler learners achieved 2.5x salary growth with average post-Scaler CTC reaching ₹23L.

11,000+placements
650+companies
Verified data
Hiring Partners:
GoogleGoogleAmazonAmazonMicrosoftMicrosoftFlipkartFlipkartAdobeAdobe1200+ more

Multiple Producers and Consumers

Everything above assumed one producer and one consumer. Scale up to several of each and the semaphore logic doesn't change, in and out still move safely because mutex serialises every buffer access regardless of how many threads are contending for it. What does need a second look: if producers also share some counter or piece of bookkeeping among themselves (a shared next-item ID, say), that shared state needs its own protection the same way the buffer does, it doesn't get it for free just because the buffer's mutex exists. The array-based circular buffer with in/out indices used in the code above generalises cleanly, the indices themselves are only ever touched inside the mutex-protected region, by design.

Semaphores vs Monitors

Semaphores aren't the only tool for this problem. Monitors bundle the shared data, the procedures that touch it, and the synchronisation into one language-level construct, so the mutual exclusion is automatic rather than something you have to remember to code correctly every single time.

SemaphoresMonitors
Mutual exclusionManual, you write wait/signal around every access yourselfAutomatic, built into entering the monitor
Easiest mistakeWrong wait() order (see the deadlock above), or forgetting a signal entirelyForgetting to call wait() on the right condition variable inside
Language support neededNone, works as a plain OS/library primitiveYes, needs compiler or runtime support (Java's synchronized, condition variables)
Where you'll actually meet itPOSIX threads, kernel code, C/C++ systems programmingJava, C#, and most higher-level concurrent languages

Neither is strictly better, semaphores are lower-level and more error-prone but work everywhere; monitors are safer by construction but only where the language gives you one.

Turn Learning into Career Growth

1200+Hiring Partners
89%Placement Rate
11,000+Placements
147%Avg Salary Increment
2.5XCareer Growth
₹23 LPAAvg Post-Scaler Salary
1200+Hiring Partners
89%Placement Rate
11,000+Placements
147%Avg Salary Increment
2.5XCareer Growth
₹23 LPAAvg Post-Scaler Salary

Common Mistakes That Cause Deadlock or Race Conditions

● Taking wait(mutex) before checking the resource semaphore (wait(empty) or wait(full)), the deadlock traced above.

● Forgetting to initialise mutex to 1. Starting it at 0 blocks the first process forever.

● Signalling the wrong semaphore, for instance the producer calling signal(empty) instead of signal(full) after adding an item. The counts stop matching reality even though nothing crashes immediately.

● Using a plain shared counter instead of empty/full semaphores to track fill level, the exact lost-update bug demonstrated above.

● Assuming a single mutex is enough on its own. Mutex alone stops concurrent access, it does nothing to stop the producer writing into a full buffer or the consumer reading an empty one, you still need empty and full for that.

Learn More About C Programming Tutorial

To better understand the producer-consumer problem in C. We must be familiar with the C programming language.

The C programming language is one of the most primitive coding languages that follow the procedural approach of programming. C programming is composed of various functions defined together to form the entire program.

To learn more about the C Programming language, refer here

FAQs

Why does the producer-consumer problem need three semaphores instead of one?

One mutex only enforces mutual exclusion, it stops two processes touching the buffer at once, but it can't stop the producer overflowing a full buffer or the consumer underflowing an empty one. empty and full track those two conditions separately, mutex handles exclusion on top of them.

What is the initial value of the mutex semaphore?

1. It represents an unlocked, available critical section. If it started at 0, the first process to call wait(mutex) would block forever, since nothing would ever signal it.

What causes a race condition in the producer-consumer problem?

Two processes reading and writing shared state, like a buffer fill-count, without exclusive access. Because an operation like count++ is really three separate steps (read, add, write back), an interleaved execution between two processes can silently drop one process's update. The demonstration above lost roughly half of 40 million increments to exactly this.

What deadlock can occur in a producer-consumer solution, and how is it avoided?

Taking the mutex before checking the resource semaphore causes it: a process can end up holding the mutex while blocked waiting on empty or full, and the process that could unblock it needs that same mutex to proceed. It's avoided by always checking the resource semaphore (wait(empty) or wait(full)) before taking the mutex, never after.

Can the producer-consumer problem be solved without semaphores?

Yes. Monitors with condition variables solve the same problem with the mutual exclusion handled automatically by the language runtime instead of by hand. Hardware-level solutions (atomic test-and-set instructions) can also work for the mutual-exclusion part, though they still need something like empty/full to handle the buffer-capacity conditions.

Does the producer-consumer solution change with multiple producers and consumers?

The core semaphore logic doesn't change, mutex still serialises all buffer access regardless of how many threads are involved. What does need attention is any additional shared state the producers or consumers keep among themselves outside the buffer, that needs its own protection too.

Conclusion

The producer-consumer problem comes down to three semaphores doing three separate jobs: empty and full track capacity in opposite directions, and mutex protects the moment either side actually touches the buffer. Get the order of wait() calls wrong and you get a deadlock, not a crash, which is exactly why it's worth tracing by hand once rather than memorising the code. If you're building on this, the dining philosophers problem is the natural next synchronisation problem to work through, and deadlock in operating systems covers the failure mode this page only touched on in the wait-order example above.

For a broader run at OS fundamentals, including where this fits alongside scheduling and memory management, Scaler's free operating systems course covers it end to end.