Header Ads

Implement a producer-consumer problem using semaphores

 This is one of the most common RTOS and Embedded C interview questions.


Problem Statement

There are two tasks:

  • Producer Task → Produces data
  • Consumer Task → Consumes data

They communicate through a shared buffer.

Requirements:

  • Producer should not write when the buffer is full.
  • Consumer should not read when the buffer is empty.
  • Both should not access the buffer simultaneously.

To solve this, we use three semaphores:

  1. Mutex → Protects the shared buffer.
  2. Empty → Counts available empty slots.
  3. Full → Counts filled slots.

Example

Buffer size = 5

Initially

Buffer = [ _ _ _ _ _ ]

Empty = 5
Full  = 0
Mutex = 1

Synchronization

Producer

Wait(Empty)
Wait(Mutex)

Add item

Signal(Mutex)
Signal(Full)

Consumer

Wait(Full)
Wait(Mutex)

Remove item

Signal(Mutex)
Signal(Empty)

C Implementation (Generic RTOS)

#include <stdio.h>
#include <pthread.h>
#include <semaphore.h>

#define BUFFER_SIZE 5

int buffer[BUFFER_SIZE];

int in = 0;
int out = 0;

sem_t empty;
sem_t full;
sem_t mutex;

void* producer(void* arg)
{
    int item = 1;

    while (1)
    {
        sem_wait(&empty);
        sem_wait(&mutex);

        buffer[in] = item;
        printf("Produced %d at %d\n", item, in);

        in = (in + 1) % BUFFER_SIZE;
        item++;

        sem_post(&mutex);
        sem_post(&full);

        sleep(1);
    }
}

void* consumer(void* arg)
{
    while (1)
    {
        sem_wait(&full);
        sem_wait(&mutex);

        int item = buffer[out];

        printf("Consumed %d from %d\n", item, out);

        out = (out + 1) % BUFFER_SIZE;

        sem_post(&mutex);
        sem_post(&empty);

        sleep(2);
    }
}

int main()
{
    pthread_t p, c;

    sem_init(&empty, 0, BUFFER_SIZE);
    sem_init(&full, 0, 0);
    sem_init(&mutex, 0, 1);

    pthread_create(&p, NULL, producer, NULL);
    pthread_create(&c, NULL, consumer, NULL);

    pthread_join(p, NULL);
    pthread_join(c, NULL);

    return 0;
}

Execution Example

Produced 1
Produced 2
Produced 3

Consumed 1

Produced 4
Produced 5

Consumed 2
Consumed 3

Produced 6

Semaphore Values During Execution

Initially

SemaphoreValue
Empty5
Full0
Mutex1

After Producer inserts one item

SemaphoreValue
Empty4
Full1
Mutex1

After Consumer removes one item

SemaphoreValue
Empty5
Full0
Mutex1

Why Three Semaphores?

1. Empty Semaphore

Keeps track of available buffer slots.

Producer waits if Empty == 0

Buffer Full

XXXXX

Producer blocks.


2. Full Semaphore

Keeps track of produced items.

Consumer waits if Full == 0

Buffer Empty

_____

Consumer blocks.


3. Mutex

Protects the critical section.

Without mutex

Producer
    |
Buffer
    |
Consumer

Both may modify buffer indices simultaneously, leading to race conditions.


RTOS Version (FreeRTOS)

SemaphoreHandle_t xMutex;
SemaphoreHandle_t xEmpty;
SemaphoreHandle_t xFull;

Producer

xSemaphoreTake(xEmpty, portMAX_DELAY);
xSemaphoreTake(xMutex, portMAX_DELAY);

/* Add item to buffer */

xSemaphoreGive(xMutex);
xSemaphoreGive(xFull);

Consumer

xSemaphoreTake(xFull, portMAX_DELAY);
xSemaphoreTake(xMutex, portMAX_DELAY);

/* Remove item */

xSemaphoreGive(xMutex);
xSemaphoreGive(xEmpty);

AUTOSAR/OSEK Perspective

OSEK/AUTOSAR OS does not provide POSIX-style semaphores. Instead, similar synchronization is achieved using:

  • Resources (GetResource() / ReleaseResource()) for mutual exclusion.
  • Events (WaitEvent() / SetEvent()) for task synchronization.
  • Counters and Alarms for timing-related activation.

The producer-consumer concept remains the same, but the implementation uses AUTOSAR OS primitives rather than semaphores.


Time Complexity

  • Produce: O(1)
  • Consume: O(1)
  • Semaphore operations: O(1)

Interview Answer (2 Minutes)

"The producer-consumer problem demonstrates synchronization between two concurrent tasks sharing a bounded buffer. The producer adds items, while the consumer removes them. To avoid race conditions and buffer overflow/underflow, we use three synchronization primitives: a mutex to ensure exclusive access to the shared buffer, an empty semaphore to count available buffer slots, and a full semaphore to count available items. The producer waits on the empty semaphore before producing and signals the full semaphore after adding an item. The consumer waits on the full semaphore before consuming and signals the empty semaphore after removing an item. This guarantees thread-safe access, prevents overflow and underflow, and is a classic synchronization pattern used in RTOS-based systems."