Part I: Foundations

This section introduces the fundamental concepts of concurrent programming. You will learn what concurrency is, why it matters, and how threads provide the foundation for parallel execution.

Prerequisites

Before beginning this tutorial, you should have:

  • A C compiler with C11 or later support

  • Familiarity with basic C++ concepts: functions, classes, and lambdas

  • Understanding of how programs execute sequentially

Why Concurrency Matters

Modern computers have multiple processor cores. A quad-core laptop can do four things at once. But most programs use only one core, leaving the others idle. Concurrency lets you use all your processing power.

Consider downloading a large file. Without concurrency, your application freezes—the user interface becomes unresponsive because your single thread of execution is busy waiting for network data. With concurrency, one thread handles the download while another keeps the interface responsive. The user can continue working, cancel the download, or start another—all while data streams in.

The benefits compound in computationally intensive work. Image processing, scientific simulations, video encoding—these tasks can be split into independent pieces. Process them simultaneously and your program finishes in a fraction of the time.

But concurrency is not free. It introduces complexity. Multiple threads accessing the same data can corrupt it. Threads waiting on each other can freeze forever. These problems—race conditions and deadlocks—are the challenges you will learn to handle.

Threads—Your Program’s Parallel Lives

When you run a program, the operating system creates a process for it. This process gets its own memory space, its own resources, and at least one thread of execution—the main thread.

Think of a thread as a bookmark in a book of instructions. It marks where you are in the code. The processor reads the instruction at that bookmark, executes it, and moves the bookmark forward. One thread means one bookmark—your program can only be at one place in the code at a time.

But you can create additional threads. Each thread is its own bookmark, tracking its own position in the code. Now your program can be at multiple places simultaneously. Each thread has its own call stack—its own record of which functions called which—but all threads share the same heap memory.

This sharing is both the power and the peril of threads.

Creating Threads

The <thread> header provides std::thread, the standard way to create threads in C++.

#include <iostream>
#include <thread>

void say_hello()
{
    std::cout << "Hello from a new thread!\n";
}

int main()
{
    std::thread t(say_hello);
    t.join();
    std::cout << "Back in the main thread.\n";
    return 0;
}

The std::thread constructor takes a function (or any callable) and immediately starts a new thread running that function. Two bookmarks now move through your code simultaneously.

The join() call makes the main thread wait until thread t finishes. Without it, main() might return and terminate the program before say_hello() completes. Always join your threads before they go out of scope.

Parallel Execution

#include <iostream>
#include <thread>

void count_up(char const* name)
{
    for (int i = 1; i <= 5; ++i)
        std::cout << name << ": " << i << "\n";
}

int main()
{
    std::thread alice(count_up, "Alice");
    std::thread bob(count_up, "Bob");

    alice.join();
    bob.join();

    return 0;
}

Run this and you might see output like:

Alice: 1
Bob: 1
Alice: 2
Bob: 2
Alice: 3
...

Or perhaps:

AliceBob: : 1
1
Alice: 2
...

The interleaving varies each run. Both threads race to print, and their outputs jumble together. This unpredictability is your first glimpse of concurrent programming’s fundamental challenge: when threads share resources (here, std::cout), chaos can ensue.

Ways to Create Threads

Threads accept any callable object: functions, lambda expressions, function objects (functors), and member functions.

Lambda Expressions

Lambda expressions are often the clearest choice:

#include <iostream>
#include <thread>

int main()
{
    int x = 42;

    std::thread t([x]() {
        std::cout << "The value is: " << x << "\n";
    });

    t.join();
    return 0;
}

The lambda captures x by value—it copies x into the lambda. By default, std::thread copies all arguments passed to it. Even if your function declares a reference parameter, the thread receives a copy.

To pass by reference, use std::ref():

#include <iostream>
#include <thread>

void increment(int& value)
{
    ++value;
}

int main()
{
    int counter = 0;

    std::thread t(increment, std::ref(counter));
    t.join();

    std::cout << "Counter is now: " << counter << "\n";
    return 0;
}

Without std::ref(), the thread would modify a copy, leaving counter unchanged.

Member Functions

For member functions, pass a pointer to the function and an instance:

#include <iostream>
#include <thread>
#include <string>

class Greeter
{
public:
    void greet(std::string const& name)
    {
        std::cout << "Hello, " << name << "!\n";
    }
};

int main()
{
    Greeter g;
    std::thread t(&Greeter::greet, &g, "World");
    t.join();
    return 0;
}

The &Greeter::greet syntax names the member function; &g provides the instance to call it on.

Thread Lifecycle: Join, Detach, and Destruction

Every thread must be either joined or detached before its std::thread object is destroyed. Failing to do so calls std::terminate(), abruptly ending your program.

join()

join() blocks the calling thread until the target thread finishes. This is how you wait for work to complete:

std::thread t(do_work);
// ... do other things ...
t.join();  // wait for do_work to finish

detach()

Sometimes you want a thread to run independently, continuing even after the std::thread object is destroyed. That is what detach() does:

std::thread t(background_task);
t.detach();  // thread runs independently
// t is now "empty"—no longer associated with a thread

A detached thread becomes a daemon thread. It runs until it finishes or the program exits. You lose all ability to wait for it or check its status. Use detachment sparingly—usually for fire-and-forget background work.

Checking joinable()

Before joining or detaching, you can check if a thread is joinable:

std::thread t(some_function);

if (t.joinable())
{
    t.join();
}

A thread is joinable if it represents an actual thread of execution. After joining or detaching, or after default construction, a std::thread is not joinable.

You have now learned the basics of threads: creation, execution, and lifecycle management. In the next section, you will learn about the dangers of shared data and how to protect it with synchronization primitives.