Bridging Go’s Elegance with C’s Power: Implementing Channels in C

Leonardo
3 min readJun 13, 2024

Golang has garnered attention in the developer community for its straightforward yet robust approach to concurrency using goroutines and channels. Driven by a desire to replicate the same magic in C, I embarked on a journey to create Go-inspired channels. This article explores the core of this implementation.

Channels in Go: A Quick Refresher

In Go, channels serve as communication bridges between goroutines. They enable data to be sent and received between these lightweight threads, ensuring synchronized access. This mechanism bypasses many traditional concurrency challenges.

Translating the Concept to C

While Go inherently supports channels, in C, we need to craft them using the building blocks provided by the POSIX threads (or pthreads) library, namely mutexes and condition variables.

Here’s the structure that forms the heart of our C-based channel:

typedef struct
{
int buffer[CHANNEL_SIZE];
int front, rear;
int count;
pthread_mutex_t mutex;
pthread_cond_t notEmpty;
pthread_cond_t notFull;
} channel;

This structure captures:

  • A circular buffer (buffer).
  • Pointers (front and rear) for data operations.
  • An element counter (count).
  • A mutex for atomic operations.
  • Condition variables for synchronization.

Channel Operations

Initialization

The channel’s initialization involves setting pointers, count, and preparing the synchronization primitives:

void channel_init(channel *ch)
{
ch->front = ch->rear = ch->count = 0;
pthread_mutex_init(&ch->mutex, NULL);
pthread_cond_init(&ch->notEmpty, NULL);
pthread_cond_init(&ch->notFull, NULL);
}

Sending and Receiving Data

Sending data involves locking the mutex, waiting if the channel is full, and then inserting the value:

void channel_send(channel *ch, int value)
{
pthread_mutex_lock(&ch->mutex);
while (ch->count == CHANNEL_SIZE)
pthread_cond_wait(&ch->notFull, &ch->mutex);
ch->buffer[ch->rear] = value;
ch->rear = (ch->rear + 1) % CHANNEL_SIZE;
ch->count++;
pthread_cond_signal(&ch->notEmpty);
pthread_mutex_unlock(&ch->mutex);
}

Receiving data is symmetrically similar:

int channel_receive(channel *ch)
{
pthread_mutex_lock(&ch->mutex);
while (ch->count == 0)
pthread_cond_wait(&ch->notEmpty, &ch->mutex);
int value = ch->buffer[ch->front];
ch->front = (ch->front + 1) % CHANNEL_SIZE;
ch->count--;
pthread_cond_signal(&ch->notFull);
pthread_mutex_unlock(&ch->mutex);
return value;
}

Putting It to the Test: Producer-Consumer Problem

The classic producer-consumer problem showcases the channel’s capability. A producer pushes data to the channel, and a consumer retrieves it. They operate concurrently, with the channel ensuring synchronization.

The producer function:

void *producer(void *arg)
{
channel *ch = (channel *)arg;
for (int i = 0; i < 20; i++)
{
channel_send(ch, i);
printf("Sent: %d\n", i);
usleep(100000);
}
return NULL;
}

The consumer function:

void *consumer(void *arg)
{
channel *ch = (channel *)arg;
for (int i = 0; i < 20; i++)
{
int value = channel_receive(ch);
printf("Received: %d\n", value);
usleep(150000);
}
return NULL;
}

Reflections and Concluding Thoughts

Building channels in C was a stimulating challenge. The project underscored C’s versatility, allowing for the replication of higher-level constructs from modern languages like Go.

There’s still a vast ocean of possibilities to explore, like extending the channel’s functionality or optimizing performance. If concurrency, channels, and bridging the old with the new excite you, let’s connect!

Source code: araujo88/channels-in-c: A simple channel implementation in C with pthreads (inspired by Go)

--

--

Leonardo

Software developer, former civil engineer. Musician. Free thinker. Writer.