Hello! Welcome to the seventh module of our course, where we pivot from single-threaded resource management to the exciting world of C++ Concurrency. Your background in OS fundamentals from OSTEP will be a significant asset here as we explore how C++ abstractions map onto the kernel features you've studied.
In the last module, we concluded with a deep dive into custom allocators, taking full control over how STL containers manage memory. Today, we begin our journey into concurrency by examining the most fundamental building block: std::thread.
This lesson is designed to address the learning outcome: Explain how std::thread maps to underlying OS primitives like POSIX threads on Linux. We won't be focusing on the full C++ API for threads just yet. Instead, true to your goal, we're going straight to the internals to understand what a std::thread is at the system level before we learn how to use it.
The Abstraction Stack: From C++ to the Kernel
std::thread is a C++11 feature that provides a portable, object-oriented interface for managing threads. However, the C++ standard library itself doesn't create threads; it acts as a wrapper around the platform's native threading capabilities.
On a modern Linux system, this creates a clear chain of abstraction:
std::thread: The high-level, RAII-capable C++ class you interact with.- POSIX Threads (
pthreads): A standardized C API for thread management on Unix-like systems. The C++ standard library on Linux (libstdc++) uses this. clone(): A Linux-specific system call that the kernel uses to create new "tasks." The C library'spthreadimplementation is built on top of this.
This isn't just theory. We can see it in action. The image below shows a debugger's backtrace for a simple C++ program that creates a std::thread.

Let's break down each layer of this stack.
Layer 1: The std::thread Concept
Before we look at the implementation, let's quickly review what a thread is. As you know from your OS studies, a thread is often called a "lightweight process." Threads within the same process run concurrently and share resources like the virtual address space (code, heap, globals), but each thread has its own private instruction pointer, register set, and stack.
This short video provides a good summary of this concept.
Back to Basics: Concurrency - Mike Shah - CppCon 2021
This clip from a CppCon talk by Mike Shah gives a concise overview of what a thread is and how it relates to a process.
Watch from 11:18 to 13:57. Pay attention to the distinction between shared resources (code, data) and per-thread resources (stack, logical control flow).
The C++ std::thread class provides a modern, portable wrapper around this concept.
Layer 2: The POSIX Threads (Pthreads) API
On Linux, the standard library implementation of std::thread doesn't talk to the kernel directly. Instead, it uses the POSIX threads (Pthreads) library, which is the standard C-level threading API on Unix-like operating systems.
The core function in the Pthreads API is pthread_create(). When you construct a std::thread object, its implementation calls pthread_create() to request a new thread from the underlying system.
The following video discusses this relationship and the role of Pthreads as a portable standard.
Back to Basics: Concurrency - Mike Shah - CppCon 2021
This segment confirms that std::thread is a portable library that is often implemented using Pthreads on Unix systems.
Watch from 14:41 to 15:51. The key takeaway is the statement that std::thread on a Unix system is likely built on top of the Pthread library.
So, std::thread is a C++ wrapper around a C API. But how does that C API, Pthreads, work on Linux? This brings us to the kernel level.
Layer 3: The Linux Kernel's "Task" Model and clone()
This is where your OS knowledge and desire for "first principles" truly come into play. A fascinating aspect of the Linux kernel's design is that it does not fundamentally distinguish between processes and threads.
To the kernel, there is only one concept: a task. A task is any schedulable entity, represented by a task_struct data structure in the kernel. What we in user-space call a "process" or a "thread" are both just tasks that have been configured with different levels of resource sharing.

The creation of all new tasks in Linux—whether for a new process (like fork()) or a new thread (pthread_create())—happens through a single, powerful system call: clone().
The behavior of clone() is controlled by a set of flags that specify which resources the new task will share with its parent. This is the key mechanism that differentiates a thread from a process.
Linux Process and Thread Creation: System Call Architecture
This article provides an excellent deep dive into how Linux creates processes and threads, focusing on the central role of the clone() system call.
Read the sections 'The Clone System Call', 'Clone Flags and Their Effects', and 'Thread Creation with pthread_create'. Focus on understanding that both threads and processes are 'tasks' and that the sharing behavior is determined by flags like CLONE_VM and CLONE_THREAD.
To summarize the key points from the article:
When pthread_create() is called, the C library (glibc) invokes the clone() system call with a specific set of flags to create what we know as a thread:
CLONE_VM: The new task shares the same virtual memory space as the parent. This is the most crucial flag for creating a thread. It's why threads can access the same global variables and heap memory.CLONE_FILES: Shares the file descriptor table. This is why a file opened in one thread can be used by another.CLONE_SIGHAND: Shares the table of signal handlers.CLONE_THREAD: Places the new task in the same "thread group" as the parent.
In contrast, when fork() is called to create a new process, it calls clone() with none of these flags set, resulting in a new task with its own separate resources (using copy-on-write for memory).
Untangling PIDs, TIDs, and Thread Groups
This "unified task" model leads to a common point of confusion regarding Process IDs (PIDs). You may have heard that on Linux, "threads are just processes," which is a statement rooted in the system's history.
Modern Linux (using the Native POSIX Thread Library, or NPTL) handles this elegantly using Thread Groups.
- Thread ID (TID): The kernel assigns a unique ID to every single task on the system. This is the "real" kernel-level process ID.
- Thread Group ID (TGID): All tasks that belong to the same process (i.e., the main thread and all threads it creates) share the same Thread Group ID. The TGID is always equal to the TID of the first thread in the group (the one that called
main()).
When you call the getpid() function in your code, it doesn't return the unique TID. It returns the TGID. This is why all threads within a single process appear to have the same PID, conforming to the POSIX standard.
Are threads implemented as processes on Linux?
This StackExchange discussion is a goldmine for understanding the history and modern reality of Linux threading. It directly addresses the TID vs. TGID distinction.
Read the answers from users 'ilkkachu', 'Lie Ryan', 'R..', and 'cweekly'. They provide the history (LinuxThreads vs. NPTL) and clarify the modern implementation, including the ps -eLf command to visualize the kernel's view.
Test your understanding!
Let's put this knowledge to the test. You can compile and run the following C++ code. The syscall(SYS_gettid) function makes a direct system call to the kernel to get the current task's unique TID.
#include <iostream>
#include <thread>
#include <unistd.h>
#include <syscall.h>
void thread_function() {
std::cout << "Child Thread: \n";
std::cout << " getpid() returns: " << getpid() << "\n";
std::cout << " syscall(SYS_gettid) returns: " << syscall(SYS_gettid) << "\n";
}
int main() {
std::thread t(thread_function);
std::cout << "Main Thread: \n";
std::cout << " getpid() returns: " << getpid() << "\n";
std::cout << " syscall(SYS_gettid) returns: " << syscall(SYS_gettid) << "\n";
t.join();
return 0;
}
Compile with: g++ -std=c++11 -pthread your_file.cpp -o main
Based on what you've learned, what do you expect the output to look like? Specifically, which values will be the same and which will be different between the main and child threads?
Show answer
You should expect to see output similar to this (the exact numbers will vary):
Main Thread:
getpid() returns: 12345
syscall(SYS_gettid) returns: 12345
Child Thread:
getpid() returns: 12345
syscall(SYS_gettid) returns: 12346
Explanation:
getpid()will return the same value for both threads. This is the Thread Group ID (TGID).syscall(SYS_gettid)will return different values. Each thread is a separate task to the kernel and has its own unique Thread ID (TID). The TGID of the group is the TID of the first thread created.
You can further verify this by running ps -eLf | grep main while your program is running (add a sleep to give yourself time). You will see two lines for your process. The PID column will be the same (the TGID), but the LWP (Light-Weight Process) column will be different, showing the unique TIDs.
Conclusion
We've peeled back the layers of abstraction to see exactly how a high-level C++ construct is realized by the operating system. This journey from a C++ object down to a kernel task_struct is a perfect example of the "under the hood" knowledge you're aiming to master.
Key Takeaways:
std::threadis a portable C++ library class that wraps platform-specific threading APIs.- On Linux,
std::threadis implemented using the Pthreads C library. - The Pthreads library, in turn, uses the Linux kernel's
clone()system call to create new threads. - The Linux kernel has a unified task model; it does not differentiate between processes and threads internally. The level of resource sharing is determined by flags passed to
clone(). - A "thread" is just a task created with flags like
CLONE_VM(shared memory) andCLONE_THREAD(shared thread group). - The user-space
getpid()returns the Thread Group ID (TGID), which is shared by all threads in a process. The kernel assigns a unique Thread ID (TID) to each individual task.
What's Next?
Now that we have a solid mental model of what a std::thread is at the system level, our next lesson will focus on the C++ API itself. We will learn how to properly create, launch, and synchronize threads using std::thread objects, covering essential operations like join() and detach(). This will be our first practical step towards building the thread pool project.