Skip to content

Wiki

std::thread

About 22 min read

Existing English translations were reused. 3 current source blocks remain in Simplified Chinese, without triggering paid translation.

std::thread is a standard thread management class provided by C++11, defined in <thread> .

This section is not only about learning to "start a thread"; more importantly, it is about understanding:

  • std::threadWhat is the relationship between the object and the actual executing thread;
  • What do the first parameter and subsequent parameters in the constructor represent respectively;
  • How are the parameters saved by default;
  • join()、detach()、joinable()What do they each change;
  • Why can the destructor of a thread object directly terminate the program;
  • Why can std::thread not be copied, but can be moved.

1.std::thread basics

1.1.std::thread The object is not the “thread itself”

可以把 std::thread 理解成一个 管理线程执行实体的 C++ 对象。

An std::thread object may be in two important states:

未关联任何执行线程
        或
关联着一个可 join 的执行线程

This state can be achieved through:

thread.joinable()

Query.

For example:

#include <print>
#include <thread>

void work()
{
    std::println("Work Done!");
}

int main()
{
    std::thread t1;
    std::println("{}", t1.joinable());

    std::thread t2(work);
    std::println("{}", t2.joinable());

    t2.join();
    std::println("{}", t2.joinable());
}

Operation result:

false
true
Work Done!
false

Note:joinable() This is not "whether the thread is still running now."

Even if the thread function has already finished executing, as long as the corresponding std::thread has not been called on it, join() or detach()it may still:

thread.joinable() == true

For example:

#include <print>
#include <thread>

using namespace std::chrono_literals;

void work()
{
    std::println("Work Done!");
}

int main()
{
    std::thread t1;
    std::println("{}", t1.joinable());

    std::thread t2(work);
    std::println("{}", t2.joinable());

    std::this_thread::sleep_for(100ms);
    std::println("{}", t2.joinable());

    t2.join();
    std::println("{}", t2.joinable());
}

Operation result:

false
true
Work Done!
true
false

Here it is obvious that the task has long since finished, but after a 100ms delay, when checkingjoinable()it is found that it is stilltrue.

Thereforejoinable()it depends on whether it can currently bejoin()ordetach(), in the above two examples: When this thread object uses the default constructor, or in other words is not given a callable object, at this time it isfalse; Also, when this thread object has already beenjoin()ordetach()it will also becomefalse.

1.2.Constructor: What is the first parameter? What are the subsequent parameters?

The most common structural form can be conceptualized as:

std::thread t(callable, arg1, arg2, ...);

Among them:

  • First parameter:the callable object to execute in the thread;
  • subsequent arguments:the actual arguments passed to this callable object.

A callable object can be:

  • Ordinary function;
  • Lambda;
  • Function object;
  • Pointer to member function.

After the thread is successfully created, the new thread will begin invoking this callable object.

1.2.1.ordinary function

#include <print>
#include <thread>

void print_sum(int a, int b)
{
    std::println("{}", a + b);
}

int main()
{
    std::thread t(print_sum, 10, 20);
    t.join();
}

Operation result:

30

Here:

print_sum

is the first parameter, namely the thread function;

10, 20

is the parameter passed to print_sum.

1.2.2.Lambda

#include <print>
#include <thread>

int main()
{
    std::thread t([](int value) {
        std::println("{}", value);
    }, 42);

    t.join();
}

Operation result:

42

Lambda itself is the first callable object parameter,42and is its argument.

1.2.3.Function object

#include <print>
#include <thread>

struct Worker
{
    void operator()(int value) const
    {
        std::println("{}", value);
    }
};

int main()
{
    std::thread t(Worker{}, 7);
    t.join();
}

Operation result:

7

1.2.4.Member function

When calling a member function, besides the member function pointer, you must also tell the thread "on which object to call it".

#include <print>
#include <thread>

class Worker
{
public:
    void run(int value)
    {
        std::println("{}", value);
    }
};

int main()
{
    Worker worker;

    std::thread t(&Worker::run, &worker, 100);
    t.join();
}

Operation result:

100

Here:

&Worker::run

is a member function pointer;

&worker

Indicates the object on which the call is made;

100

is the run(int) own parameter.

2.Parameter Passing and Lifecycle

2.1.Thread parameters are copied by default

This is std::thread one of the places most likely to confuse beginners.

For example:

void foo(int value);

int x = 10;
std::thread t(foo, x);

A thread does not simply "remember the expression x". The standard library decays and stores the passed arguments inside the thread, then invokes them in the new thread.

Therefore, in many cases, the thread obtains a copy of the parameter.

2.1.1.Pass by value

#include <print>
#include <thread>

void change(int value)
{
    value = 100;
}

int main()
{
    int x = 10;

    std::thread t(change, x);
    t.join();

    std::println("{}", x);
}

Operation result:

10

Because the thread modifies its own copy of the parameter.

2.2.std::ref():Truly pass by reference

If the thread function requires:

void change(int& value);

and want to modify the original object, you should use:

std::ref(x)

For example:

#include <functional>
#include <print>
#include <thread>

void change(int& value)
{
    value = 100;
}

int main()
{
    int x = 10;

    std::thread t(change, std::ref(x));
    t.join();

    std::println("{}", x);
}

Operation result:

100

std::ref(x) What is returned is a:

std::reference_wrapper<T>

It can itself be copied, but when the callable object is invoked, it is reduced back to a reference to the original object.

2.2.1.std::cref()

If you need to pass by const T& , you can use:

std::cref(x)

For example:

void print(const std::string& text);

std::string message = "hello";
std::thread t(print, std::cref(message));

2.3.std::ref(x) and &x are completely different matters

These two notations are often confused.

std::ref(x)

Indicates "pass the original object by reference semantics".

And:

&x

is taking the address; the type is a pointer.

For example:

void f1(int& value);
void f2(int* value);

int x = 10;

std::thread t1(f1, std::ref(x));
std::thread t2(f2, &x);

The two correspond to different parameter types.

2.4.Lambda capture also requires attention to value and reference

Threads and Lambdas are often used together.

Capture by value:

int x = 10;

std::thread t([x] {
    std::println("{}", x);
});

Lambda stores its own copy of x.

Capture by reference:

int x = 10;

std::thread t([&x] {
    x = 20;
});

t.join();

The thread directly accesses the external x。

Capturing by reference does not extend the lifetime of x, so you must ensure that when the thread uses x , x still exists.

3.Thread Termination and Joinable State

3.1.join():Wait for thread termination

t.join();

Indicates:

The current thread blocks here until the thread associated with t finishes executing.

For example:

#include <chrono>
#include <print>
#include <thread>

using namespace std::chrono_literals;

void work()
{
    std::this_thread::sleep_for(1s);
    std::println("worker done");
}

int main()
{
    std::thread t(work);

    std::println("before join");
    t.join();
    std::println("after join");
}

Operation result:

before join
worker done
after join

The execution relationship is:

创建 t
  ↓
work 与 main 并发执行
  ↓
main 到达 join
  ↓
等待 work 结束
  ↓
join 返回
  ↓
main 继续执行

After a successful call:

t.joinable() == false

3.1.1.join()does not automatically extend the lifetime of arbitrary objects

join() It only waits for the target thread at the location where it is called.

The following are safe:

int x = 10;
std::thread t([&x] {
    ++x;
});

t.join();
// 到这里线程已经结束,而 x 还活着

But if the object goes out of scope before the thread is joined, it is already too late:

std::thread t;

{
    int x = 10;
    t = std::thread([&x] {
        // 可能访问 x
    });
} // x 在这里销毁

// 此时线程可能仍在访问已经销毁的 x
t.join();

3.2.detach(): Thread Detach Management

t.detach();

Official definition: After calling detach(), the thread continues to run independently, and the current std::thread object is no longer associated with that thread.

For ease of understanding, it can also be informally understood as: detach() As soon as it is called, the current thread object will immediately become unjoinable(); It is somewhat like a "non-blocking join()"—the difference being that join() waits for the thread to finish executing before returning, whereas detach() does not wait and returns immediately after being called, allowing the thread to continue running in the background on its own.

Therefore:

t.joinable() == false

detach() The biggest problem is that the lifecycle is very difficult to manage.

Incorrect example:

void start()
{
    int value = 42;

    std::thread t([&value] {
        std::println("{}", value);
    });

    t.detach();
} // value 已经销毁,但脱离线程可能仍在使用它

This will result in a dangling reference and undefined behavior.

In beginner and ordinary engineering code, prioritize explicit join(); do not habitually detach() just to "let a thread run in the background".

3.3.joinable(): Determine whether a join is possible

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

Common state changes:

操作 joinable()
默认构造 std::thread t; false
std::thread t(f); true
线程函数已经执行完,但尚未 join/detach true
调用 join() 后 false
调用 detach() 后 false
被 move-from 的 thread false

3.4.Why does forgetting join/detach directly terminate the program?

If a std::thread object still satisfies the following when it is destroyed:

t.joinable() == true

The program will call:

std::terminate()

Thisstd::terminate()will immediately terminate the entire program. It is not "ending the current thread"; rather, the entire process directly enters an abnormal termination flow.

For example:

#include <thread>

void work()
{
}

int main()
{
    std::thread t(work);
} // 错误:t 析构时仍然 joinable

Run result: The program will, because the std::thread object still joinable() when destructed, call std::terminate(). Example output is similar to:

terminate called without an active exception

The rule can be conceptualized as:

~thread()
{
    if (joinable())
    {
        std::terminate();
    }
}

This is not standard library source code; it is only meant to help understand destructor semantics.

The standard library will not secretly and automatically help you with join(), because automatic waiting may cause the program to block for a long time at an unexpected location; nor will it secretly detach(), because that could create more serious lifetime errors.

4.Management Rights and Thread Utilities

4.1.std::thread Cannot be copied, but can be moved

The management right of a thread cannot be owned by two std::thread objects at the same time, so it is not copyable:

std::thread t1(work);

// std::thread t2 = t1; // 编译错误

But ownership can be transferred:

std::thread t1(work);
std::thread t2 = std::move(t1);

After moving:

t1.joinable() == false
t2.joinable() == true

Finally, it should be handled by t2 or join() responsible for detach().

This is also why std::vector<std::thread> can work: the container manages thread objects by moving rather than copying.

4.2.std::this_thread

std::this_thread Provides a tool to "operate on the current thread." However, it does not have to be used only within thestd::threadobject.

4.2.1.Get thread ID

std::this_thread::get_id()

Example:

#include <print>
#include <thread>

int main()
{
    std::println("{}", std::this_thread::get_id());
}

Example run result (thread IDs may differ between runs):

139779631826496

4.2.2.Sleep

std::this_thread::sleep_for(...)
std::this_thread::sleep_until(...)
Function Parameter Meaning
sleep_for(duration) Duration How much longer to sleep
sleep_until(time_point) Time point Until what time to sleep

For example:

//睡100ms
std::this_thread::sleep_for(std::chrono::milliseconds(100));
//到相对于now()之后的100ms
auto next = std::chrono::steady_clock::now();
next += 100ms;
std::this_thread::sleep_until(next);

4.2.3.voluntarily yield the execution opportunity

std::this_thread::yield();

It merely hints to the scheduler that "the current thread is willing to temporarily yield its execution opportunity"; it does not guarantee that another specified thread will run immediately, nor does it guarantee that the next thread to run will not be itself.

To put it more vividly: the current thread actively tells the operating system scheduler: "I can yield the CPU for now, and you may consider letting another thread run first." yield()It merely "voluntarily gives up this CPU scheduling opportunity," but it is entirely possible that it will immediately be scheduled back.

For example:

while (!ready) 
{
    std::this_thread::yield();
}

This is much better thanwhile (!ready) {}busy waiting is much better, because busy waiting will occupy the CPU all the time, while yield() gives other threads more opportunities to run.

4.3.hardware_concurrency()

std::thread::hardware_concurrency()

Generally used to query: approximately how many threads the current system supports for parallel execution. This is usually the number of CPU logical cores / hardware threads.

#include <print>
#include <thread>

int main()
{
    std::println("建议并发线程数:{}", std::thread::hardware_concurrency());
}

Example run result (values depend on the current machine):

建议并发线程数:16

Assume your CPU is 8 核 16 线程, then std::thread::hardware_concurrency() usually returns 16.

但这并不意味着程序最多只能创建 16 个线程。它表示的是当前硬件大约能够支持多少个线程真正并行执行,也就是硬件并发能力的参考值。 即使只有 16 个逻辑处理器,只要系统资源允许,依然可以创建几十个甚至上百个线程。

When the number of threads exceeds the number that the hardware can truly execute in parallel, the operating system uses mechanisms such as time slicing and thread scheduling to let these threads alternately obtain CPU execution time, making it appear macroscopically as if they are "running simultaneously." Although this is not true full parallelism, in many scenarios, from the perspective of program execution results, there is no particularly noticeable difference from true concurrency. (Similar to FreeRTOS used on STM32, which uses one CPU core to simulate multithreading.)

Additionally, if the function returns 0, it indicates that the current implementation cannot provide hardware concurrency information, so this return value cannot be treated as an absolutely reliable fixed value.

4.4.The output order of multiple threads is nondeterministic.

#include <print>
#include <thread>

void print(char c)
{
    for (int i = 0; i < 5; ++i)
    {
        std::print("{}", c);
    }
}

int main()
{
    std::thread t1(print, 'A');
    std::thread t2(print, 'B');

    t1.join();
    t2.join();
}

Example run result (thread scheduling differs, so the order may vary):

BBBBBAAAAA

Cannot rely on the output always being:

AAAAA
BBBBB

It is entirely possible that

BBBBB
AAAAA

and many other possibilities.

Thread scheduling order is determined by the operating system and the runtime environment. If the execution order itself is a necessary condition for program correctness, explicit synchronization is required.

5.Common Errors

5.1.Error: forgot to join or detach

std::thread t(work);
// 离开作用域前没有处理 t

The result may be std::terminate().

5.2.Error: Mistaking a reference parameter for a thread automatically passing by reference

void foo(int& x);
int value = 10;

// std::thread t(foo, value); // 不要把它理解成自动引用传递

When reference semantics to the original object are required, write:

std::thread t(foo, std::ref(value));

5.3.Error: Reference to a local variable after detach

Local variables may be destroyed before they are used by the thread.

5.4.Error: duplicate join

t.join();
// t.join(); // 此时已经不再 joinable

5.5.Error: assuming that joinable automatically becomes false after the thread function ends

A thread finishing execution and a thread object becoming disassociated are two different things.

6.C++20: Prioritize understanding of std::jthread

std::threadOne obvious risk is that all control paths must be manually ensured to eventually correctly join() or detach().

C++20 provides:

std::jthread

On destruction, it automatically requests a stop and waits for the thread to finish, and supports std::stop_token cooperative cancellation.

After understanding the basic principles std::thread, it is recommended to continue learning the subsequent std::jthread and std::stop_token in this chapter.

7.Summary

  • std::thread t(f, args...)The first parameter is a callable object, and the subsequent parameters are the arguments passed to it.
  • Thread parameters are saved by default; reference semantics usually require explicit use of std::ref() / std::cref().
  • join() Waits for the thread to finish and makes the thread object become non-joinable.
  • detach() Detaches the thread from the current std::thread object; the lifetime risk is relatively high.
  • joinable() Determines whether the object is still associated with a joinable thread; it is not equal to whether the thread is currently running.
  • A joinable std::thread destructor calls std::terminate().
  • std::thread It is not copyable, but it can be moved.
  • When threads share references, pointers, or objects, object lifetime and data races must be considered additionally.