Master C++ Concurrency: Processes, Threads, and IO Multiplexing Explained

This article explores C++ concurrent programming techniques, covering multi‑process fundamentals, process creation with fork(), inter‑process communication methods, multi‑threading using std::thread, synchronization tools like mutexes and condition variables, and efficient I/O handling through select, poll, and epoll multiplexing, with practical code examples.

Deepin Linux
Deepin Linux
Deepin Linux
Master C++ Concurrency: Processes, Threads, and IO Multiplexing Explained
Concurrency illustration
Concurrency illustration

Part1: Multi‑process – Independent “small worlds”

Multi‑process means a program runs several independent tasks, each in its own process with separate memory and resources. In Linux, processes are created with fork(), which duplicates the calling process; the child receives a return value of 0, while the parent receives the child’s PID. Example code demonstrates creating a child and printing its PID.

#include <stdio.h>
#include <unistd.h>

int main() {
    pid_t pid;
    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        printf("I am the child process, my PID is %d, my parent's PID is %d
", getpid(), getppid());
    } else {
        printf("I am the parent process, my PID is %d, my child's PID is %d
", getpid(), pid);
    }
    return 0;
}

Inter‑process communication (IPC) allows processes to exchange data. Common IPC mechanisms include pipes, message queues, and shared memory. The following snippet shows parent‑child communication via a pipe.

#include <stdio.h>
#include <unistd.h>
#include <string.h>

#define BUFFER_SIZE 1024

int main() {
    int pipe_fd[2];
    pid_t pid;
    char buffer[BUFFER_SIZE];

    if (pipe(pipe_fd) == -1) {
        perror("pipe creation failed");
        return 1;
    }

    pid = fork();
    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        close(pipe_fd[0]);
        const char *message = "Hello from child";
        write(pipe_fd[1], message, strlen(message));
        close(pipe_fd[1]);
    } else {
        close(pipe_fd[1]);
        ssize_t bytes_read = read(pipe_fd[0], buffer, BUFFER_SIZE - 1);
        if (bytes_read > 0) {
            buffer[bytes_read] = '\0';
            printf("Received from child: %s
", buffer);
        }
        close(pipe_fd[0]);
    }
    return 0;
}

Advantages of multi‑process

Process isolation: a crash in one process does not affect others, improving stability.

Clear resource allocation: each process has its own resources, simplifying management.

Disadvantages of multi‑process

Complex IPC: communication requires extra mechanisms and careful synchronization.

Higher overhead: creating and destroying processes consumes more CPU and memory.

Part2: Multi‑threading – Lightweight collaboration

Threads share the same process resources, allowing concurrent execution within a single address space. C++11 introduced std::thread for portable thread creation. Simple examples show creating a thread to print a message and using a lambda to capture external variables.

#include <iostream>
#include <thread>

void print_message() {
    std::cout << "This is a message from the thread." << std::endl;
}

int main() {
    std::thread t(print_message);
    t.join();
    return 0;
}
#include <iostream>
#include <thread>

int main() {
    int value = 42;
    std::thread t([&]() {
        std::cout << "The value is: " << value << std::endl;
    });
    t.join();
    return 0;
}

Thread synchronization

To avoid data races, C++ provides std::mutex and std::condition_variable. The following example protects a shared counter with a mutex.

#include <iostream>
#include <thread>
#include <mutex>

std::mutex mtx;
int shared_data = 0;

void increment() {
    for (int i = 0; i < 10000; ++i) {
        mtx.lock();
        ++shared_data;
        mtx.unlock();
    }
}

int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "The final value of shared_data is: " << shared_data << std::endl;
    return 0;
}

A producer‑consumer model using a condition variable demonstrates coordination between threads.

#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>

std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;

void producer() {
    for (int i = 0; i < 10; ++i) {
        std::unique_lock<std::mutex> lock(mtx);
        data_queue.push(i);
        lock.unlock();
        cv.notify_one();
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
}

void consumer() {
    while (true) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [] { return !data_queue.empty(); });
        int data = data_queue.front();
        data_queue.pop();
        lock.unlock();
        std::cout << "Consumed: " << data << std::endl;
        if (data == 9) break;
    }
}

int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
}

Advantages of multi‑threading

Easy resource sharing and communication.

Lower context‑switch overhead compared to processes.

Improved responsiveness in GUI applications.

Disadvantages of multi‑threading

Potential for race conditions and deadlocks if synchronization is misused.

Increased programming complexity and debugging difficulty.

Performance can degrade with excessive threads due to scheduling overhead.

Part3: IO Multiplexing – Efficient I/O management

IO multiplexing lets a single thread monitor many file descriptors. Linux provides select, poll, and epoll. select works on all platforms but limits the number of descriptors and copies descriptor sets on each call.

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);

Example: a simple TCP server using select to handle new connections and incoming data.

#include <iostream>
#include <sys/select.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <cstring>

#define PORT 8080
#define MAX_CLIENTS 10

int main() {
    int server_socket = socket(AF_INET, SOCK_STREAM, 0);
    if (server_socket == -1) {
        perror("socket creation failed");
        return 1;
    }
    struct sockaddr_in server_addr;
    server_addr.sin_family = AF_INET;
    server_addr.sin_port = htons(PORT);
    server_addr.sin_addr.s_addr = INADDR_ANY;
    if (bind(server_socket, (struct sockaddr *)&server_addr, sizeof(server_addr)) == -1) {
        perror("bind failed");
        close(server_socket);
        return 1;
    }
    if (listen(server_socket, 3) == -1) {
        perror("listen failed");
        close(server_socket);
        return 1;
    }
    fd_set read_fds;
    FD_ZERO(&read_fds);
    FD_SET(server_socket, &read_fds);
    int max_fd = server_socket;
    while (true) {
        fd_set temp_fds = read_fds;
        int activity = select(max_fd + 1, &temp_fds, NULL, NULL, NULL);
        if (activity == -1) {
            perror("select error");
            break;
        } else if (activity > 0) {
            if (FD_ISSET(server_socket, &temp_fds)) {
                int client_socket = accept(server_socket, NULL, NULL);
                if (client_socket != -1) {
                    FD_SET(client_socket, &read_fds);
                    if (client_socket > max_fd) max_fd = client_socket;
                }
            }
            for (int i = 0; i <= max_fd; ++i) {
                if (FD_ISSET(i, &temp_fds) && i != server_socket) {
                    char buffer[1024] = {0};
                    int valread = read(i, buffer, sizeof(buffer));
                    if (valread <= 0) {
                        close(i);
                        FD_CLR(i, &read_fds);
                    } else {
                        std::cout << "Received: " << buffer << std::endl;
                    }
                }
            }
        }
    }
    close(server_socket);
    return 0;
}
poll

removes the descriptor limit and separates parameters, but still copies large arrays between user and kernel space.

#include <poll.h>

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
epoll

(Linux‑specific) offers the best performance for large numbers of descriptors, using an event‑driven model.

#include <sys/epoll.h>

int epoll_create(int size); // size is ignored after Linux 2.6.8
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

Choosing the right mechanism depends on portability, descriptor count, and performance requirements.

Part4: Comparing the three approaches

Multi‑process provides strong isolation suitable for critical server components; multi‑threading offers lightweight sharing for tightly coupled tasks; IO multiplexing excels at handling massive concurrent I/O with minimal threads. In real C++ projects, developers must weigh these trade‑offs to select the most appropriate concurrency model for efficiency and stability.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Cexample-codeLinuxprocessesIO MultiplexingThreads
Deepin Linux
Written by

Deepin Linux

Research areas: Windows & Linux platforms, C/C++ backend development, embedded systems and Linux kernel, etc.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.