Section 5: Pipes and Multithreading

Warmup: A fork() in the Road

Consider the following C code. This C code should create 20 child processes, each of which print a message.

#include <stdio.h>

int main() {
    pid_t pid;

    for (int i = 0; i < 20; i++) {
        pid = fork();

        if (pid == 0) {
            printf("Hello from pid %d\n", getpid());
        }
    }

    return 0;
}

WARMUP A. Unfortunately, the C code is buggy! Identify the bug and fix them.

WARMUP B. Why is this bug particularly problematic? (Hint: Think about what resources are used in the fork() system call.)

Question 1: Piping Hot!

Consider the following C code that wants to set up a pipe between two child processes, send a message from one, and have the other recieve it:

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

int main() {

    pid_t pid_1 = fork();

    if (pid_1 == 0) {
        int pipe_fds[2];
        int ret = pipe(pipe_fds);
        close(pipe_fds[0]);

        const char* message = "Hello from the child!";
        printf("[Child: %d] Sending message: %s\n", getpid(), message);
        write(pipe_fds[1], message, strlen(message));

        close(pipe_fds[1]);
        return 0;
    }

    pid_t pid_2 = fork();

    if (pid_2 == 0) {
        int pipe_fds[2];
        int ret = pipe(pipe_fds);
        close(pipe_fds[1]);
        char buffer[512];

        if (read(pipe_fds[0], buffer, 512) > 0) {
            printf("[Child: %d] Recieved message: %s\n", getpid(), buffer);
        }

        close(pipe_fds[0]);
        return 0;
    }

    for (int i = 0; i < 2; i++) {
        pid_t ret_pid = waitpid(-1, NULL, 0);
        printf("[Parent: %d] Child %d has exited\n", getpid(), ret_pid);
    }

    printf("[Parent: %d] All child processes have exited\n", getpid());
    return 0;
}

QUESTION 1A. What output would we expect the above code to produce?

QUESTION 1B. Unfortunately, the above code is buggy and does not work as intended. What output will actually it produce?

QUESTION 1C. How can we change the above code so child processes can actually communicate through a pipe?

Question 2: Threading The Threads

Recall that std::thread's constructor takes the name of the function to run on the new thread and any arguments, like std::thread t(func, param1, ...). Given a thread t, t.join() blocks the current thread until thread t terminates. t.detach() sets the t thread freeā€”it can no longer be joined. Each thread object must be detached or joined before it goes out of scope (is destroyed).

QUESTION 2A. What can this code print, and how many threads might run concurrently during its execution?

int main() {
    for (int i = 0; i != 3; ++i) {
        std::thread t(printf, "%d\n", i + 1);
        t.join();
    }
}

QUESTION 2B. Unlike join(), detach() on a thread object detaches the thread represented and lets it run independently, never to be joined. What can this code print, and how many threads might run concurrently during its execution? [Hint: consider what will happen when the main thread of the process exits.]

int main() {
    for (int i = 0; i != 3; ++i) {
        std::thread t(printf, "%d\n", i + 1);
        t.detach();
    }
}

QUESTION 2C. What can this code print?

int main() {
    for (int i = 0; i != 3; ++i) {
        std::thread t(printf, "%d\n", i + 1);
    }
}

Question 3: Counting, Right or Wrong?

In the following, we will look at a set of C++ programs that increment variables in multiple threads. For each, consider whether a race condition occurs or not.

QUESTION 3A: Does this program contain a race condition? What does it print?

#include <thread>
#include <cstdio>

void count(int x) {
    int n = 0;

    printf("%d\n", n + 1);
}

int main() {
    /* M1 */
    std::thread t1(count, 0);
    std::thread t2(count, 1);
    std::thread t3(count, 2);
    /* M2 */
    t3.join();
    t2.join();
    t1.join();
}

QUESTION 3B: Does this program contain a race condition? What does it print?

#include <thread>
#include <cstdio>

void count(int n) {
    printf("%d\n", n + 1);
}

int main() {
    /* M1 */
    std::thread t1(count, 0);
    std::thread t2(count, 1);
    std::thread t3(count, 2);
    /* M2 */
    t3.join();
    t2.join();
    t1.join();
}

QUESTION 3C: Does this program contain a race condition? What does it print?

#include <thread>
#include <cstdio>

int n;

void count(int* n) {
    *n += 1;
    printf("%d\n", *n);
}

int main() {
    n = 0;
    /* M1 */
    std::thread t1(count, &n);
    std::thread t2(count, &n);
    std::thread t3(count, &n);
    /* M2 */
    t3.join();
    t2.join();
    t1.join();
}