Skip to content

Last updated:2026-10-12

C++20 channel in Practice

C++20 in Practice: channel

In the previous two live streams we covered the usage and underlying implementation of thread, jthread, stop_token, stop_source, stop_callback, cv, cv_any, and more. So how can we turn this knowledge into a small project?

Which brings us to this article: writing a channel.

1. Design

1.1 Interface Design

At the interface level we expect the same as a general-purpose channel: creating a sending and a receiving channel in one shot.

For example:

auto [sender, receiver] = make_channel<int>();

The implementation can expose an outward-facing interface similar to make_unique; the return values are obtained via C++17 structured bindings, and at that point we have two channels.

The two most essential interfaces: send and receive.

  • Send
 sender.send(counter++);
  • Receive
auto value = receiver.recv();

Besides that, a shutdown interface can be provided at destruction time, so that blocked send/recv calls can be woken up and released.

For example:

bool shutdown() {
    if (!state_) {
        return false;
    }

    light_city_lock lk(state_->mutex);
    if (source_.stop_requested()) {
        return false;
    }
    source_.request_stop();
    state_->cv.notify_all();
    return true;
}

1.2 Internal Queue Design

For the internal queue, refer to the article published a few days ago on designing a thread-safe multi-producer multi-consumer queue: here we can maintain a state internally that carries a queue, a mutex, and a cv_any.

struct State {
    std::queue<T> queue;
    std::mutex mutex;
    // C++11 condition_variable_any
    std::condition_variable_any cv;
};

2. Usage

It is very easy to use: just make the two channels, then send and recv.

Here I used jthread + stop_token to do the work, for example:

std::jthread sender_thread(
    [](Channel<int> sender, std::stop_token token) {
      int counter = 0;
      while (!token.stop_requested()) {
        sender.send(counter++);
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
      }
    },
    std::move(sender), token);

The receiving thread is similar. Once things have run about long enough, we can use

stop_source.request_stop();

At this point a channel has been simulated: the sending side sends data, and the receiving side just receives it.

Sample run:

➜  channel ./a.out                  
Received: 0
Received: 1
Received: 2
Received: 3
Received: 4
Received: 5
Received: 6
Received: 7
Received: 8
Received: 9

Comments