跳到正文

Wiki

std::thread

约 10 分钟阅读

std::thread 是 C++11 提供的标准线程管理类,定义在 <thread> 中。

这一节不只要学会“开一个线程”,更重要的是理解:

  • std::thread 对象和实际执行线程是什么关系;
  • 构造函数里第一个参数和后续参数分别表示什么;
  • 参数默认是如何保存的;
  • join()detach()joinable() 分别改变了什么;
  • 为什么线程对象的析构函数可能直接终止程序;
  • 为什么 std::thread 不能复制,但可以移动。

1.std::thread 基础

1.1.std::thread 对象不是“线程本身”

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

一个 std::thread 对象可能处于两种重要状态:

未关联任何执行线程

关联着一个可 join 的执行线程

这个状态可以通过:

thread.joinable()

查询。

例如:

#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());
}

运行结果:

false
true
Work Done!
false

注意:joinable() 并不是“线程现在是不是还在运行”。

即使线程函数已经执行完,只要还没有对对应的 std::thread 调用 join()detach(),它仍然可能:

thread.joinable() == true

比如:

#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());
}

运行结果:

false
true
Work Done!
true
false

这里明显任务早就结束了,但延时100ms后去看joinable()发现还是true

所以joinable()看的是他现在能不能被join()或者detach(),上面两个例子中: 当这个线程对象使用默认构造,或者说没有被给一个可调用对象的时候,这时候是false; 还有这个线程对象已经被join()或者detach()的时候,也会变成false

1.2.构造函数:第一个参数是什么?后面的参数是什么?

最常见的构造形式可以概念化为:

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

其中:

  • 第一个参数:要在线程中执行的可调用对象;
  • 后续参数:传给这个可调用对象的实参。

可调用对象可以是:

  • 普通函数;
  • Lambda;
  • 函数对象;
  • 成员函数指针。

线程创建成功后,新线程会开始调用这个可调用对象。

1.2.1.普通函数

#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();
}

运行结果:

30

这里:

print_sum

是第一个参数,即线程函数;

10, 20

是传给 print_sum 的参数。

1.2.2.Lambda

#include <print>
#include <thread>

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

    t.join();
}

运行结果:

42

Lambda 本身就是第一个可调用对象参数,42 是它的实参。

1.2.3.函数对象

#include <print>
#include <thread>

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

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

运行结果:

7

1.2.4.成员函数

调用成员函数时,除了成员函数指针,还必须告诉线程“在哪个对象上调用”。

#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();
}

运行结果:

100

这里:

&Worker::run

是成员函数指针;

&worker

表示在哪个对象上调用;

100

才是 run(int) 自己的参数。

2.参数传递与生命周期

2.1.线程参数默认会保存副本

这是 std::thread 最容易让初学者误解的地方之一。

例如:

void foo(int value);

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

线程并不是简单地“记住表达式 x”。标准库会把传入参数经过衰减后保存进线程内部,再在新线程中调用。

因此很多情况下,线程拿到的是参数副本。

2.1.1.按值传递

#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);
}

运行结果:

10

因为线程修改的是自己的参数副本。

2.2.std::ref():真正按引用传递

如果线程函数要求:

void change(int& value);

并希望修改原对象,应使用:

std::ref(x)

例如:

#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);
}

运行结果:

100

std::ref(x) 返回的是一个:

std::reference_wrapper<T>

它本身可以被复制,但调用可调用对象时会还原为对原对象的引用。

2.2.1.std::cref()

如果需要按 const T& 传递,可以使用:

std::cref(x)

例如:

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

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

2.3.std::ref(x)&x 完全不是一回事

这两个写法经常被混淆。

std::ref(x)

表示“以引用语义传递原对象”。

而:

&x

是取地址,类型是指针。

例如:

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

int x = 10;

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

两者对应的是不同的形参类型。

2.4.Lambda 捕获也要注意值和引用

线程和 Lambda 经常配合使用。

按值捕获:

int x = 10;

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

Lambda 内保存自己的 x 副本。

按引用捕获:

int x = 10;

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

t.join();

线程直接访问外部的 x

按引用捕获不会延长 x 的生命周期,因此必须确保线程使用 x 时,x 仍然存在。

3.线程收尾与 joinable 状态

3.1.join():等待线程结束

t.join();

表示:

当前线程在这里阻塞,直到 t 所关联的线程执行结束。

例如:

#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");
}

运行结果:

before join
worker done
after join

执行关系是:

创建 t

work 与 main 并发执行

main 到达 join

等待 work 结束

join 返回

main 继续执行

调用成功后:

t.joinable() == false

3.1.1.join() 不会自动延长任意对象生命周期

join() 只是在它被调用的位置等待目标线程。

下面是安全的:

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

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

但如果对象先离开作用域,再去等待线程,就已经太迟:

std::thread t;

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

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

3.2.detach():线程脱离管理

t.detach();

官方定义:调用 detach() 后,线程会继续独立运行,而当前 std::thread 对象不再与该线程关联。

为了方便理解,也可以“民办”地理解成: detach() 一调用,当前线程对象就会立刻变成不可 joinable(); 它有点像一个“不会阻塞的 join()”——区别在于,join() 会等待线程执行结束后再返回,而 detach() 不会等待,调用后立即返回,让线程自己在后台继续运行。

因此:

t.joinable() == false

detach() 最大的问题是生命周期很难管理。

错误示例:

void start()
{
    int value = 42;

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

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

这会产生悬空引用和未定义行为。

初学和普通工程代码中,优先考虑明确 join(),不要为了“让线程后台跑”就习惯性 detach()

3.3.joinable():判断能否 join

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

常见状态变化:

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

3.4.为什么忘记 join/detach 会直接终止程序?

如果一个 std::thread 对象析构时仍满足:

t.joinable() == true

程序会调用:

std::terminate()

这个std::terminate()会立刻终止整个程序。它不是“结束当前线程”,而是整个进程直接进入异常终止流程。

例如:

#include <thread>

void work()
{
}

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

运行结果: 程序会因为 std::thread 对象析构时仍然 joinable() 而调用 std::terminate(),示例输出类似:

terminate called without an active exception

可以把规则概念化成:

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

这不是标准库源码,只是帮助理解析构语义。

标准库不会偷偷帮你自动 join(),因为自动等待可能让程序在一个意料之外的位置长时间阻塞;也不会偷偷 detach(),因为那可能制造更严重的生命周期错误。

4.管理权与线程工具

4.1.std::thread 不可复制,但可以移动

线程的管理权不能被两个 std::thread 对象同时拥有,因此它不可复制:

std::thread t1(work);

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

但可以转移所有权:

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

移动后:

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

最后应该由 t2 负责 join()detach()

这也是为什么 std::vector<std::thread> 可以工作:容器通过移动而不是复制管理线程对象。

4.2.std::this_thread

std::this_thread 提供“操作当前线程”的工具。但他不是只能在std::thread的对象内使用。

4.2.1.获取线程 ID

std::this_thread::get_id()

示例:

#include <print>
#include <thread>

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

运行结果示例(线程 ID 每次运行可能不同):

139779631826496

4.2.2.休眠

std::this_thread::sleep_for(...)
std::this_thread::sleep_until(...)
函数 参数 含义
sleep_for(duration) 时间长度 再睡多久
sleep_until(time_point) 时间点 睡到什么时候

例如:

//睡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.主动让出执行机会

std::this_thread::yield();

它只是向调度器提示“当前线程愿意暂时让出执行机会”,并不保证另一个指定线程一定马上运行,也不保证下一个运行的线程不是自己。

比较形象一点的说法:当前线程主动告诉操作系统调度器:“我现在可以先让一下 CPU,你可以考虑让别的线程先运行。” yield() 只是“主动让出这次 CPU 调度机会”,但马上又被调度回来也是完全可能的。

比如:

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

这种比while (!ready) {}这种空转要好很多,因为空转会一直霸占 CPU,而 yield() 会给其他线程更多运行机会。

4.3.hardware_concurrency()

std::thread::hardware_concurrency()

一般用来查询:当前系统大概支持多少个线程可以并行执行。一般是CPU 的逻辑核心数 / 硬件线程数。

#include <print>
#include <thread>

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

运行结果示例(数值取决于当前机器):

建议并发线程数:16

假设你的 CPU 是 8 核 16 线程,那么 std::thread::hardware_concurrency() 通常会返回 16

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

当线程数量超过硬件能够真正并行执行的数量时,操作系统会通过时间片轮转、线程调度等机制,让这些线程交替获得 CPU 执行时间,从宏观上看起来像是在“同时运行”。虽然这不是真正意义上的完全并行,但在很多场景下,从程序运行效果上看和真正并发并没有特别明显的区别。(类似STM32上使用的FreeRTOS,用1个CPU内核来模拟多线程)

另外,如果函数返回 0,表示当前实现无法提供硬件并发信息,因此不能把这个返回值当作绝对可靠的固定值。

4.4.多线程输出顺序是不确定的

#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();
}

运行结果示例(线程调度不同,顺序可能不同):

BBBBBAAAAA

不能依赖输出一定是:

AAAAA
BBBBB

完全有可能是

BBBBB
AAAAA

等很多可能性。

线程调度顺序由操作系统和运行环境决定。如果执行顺序本身是程序正确性的必要条件,就必须显式同步。

5.常见错误

5.1.错误:忘记 join 或 detach

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

结果可能是 std::terminate()

5.2.错误:把引用形参误认为 thread 会自动按引用传递

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

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

需要原对象引用语义时写:

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

5.3.错误:detach 后引用局部变量

局部变量可能在线程使用前就销毁。

5.4.错误:重复 join

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

5.5.错误:认为线程函数结束后 joinable 自动变 false

线程执行完和线程对象解除关联是两件不同的事情。

6.C++20:优先了解 std::jthread

std::thread 的一个明显风险是:必须手动确保所有控制路径最终都正确 join()detach()

C++20 提供了:

std::jthread

它在析构时会自动请求停止并等待线程结束,而且支持 std::stop_token 协作式取消。

基础原理理解完 std::thread 后,建议继续学习本章后面的 std::jthreadstd::stop_token

7.小结

  • std::thread t(f, args...) 的第一个参数是可调用对象,后面的参数是传给它的实参。
  • 线程参数默认会被保存,引用语义通常要显式使用 std::ref() / std::cref()
  • join() 等待线程结束,并让线程对象变为 non-joinable。
  • detach() 让线程脱离当前 std::thread 对象,生命周期风险较大。
  • joinable() 判断对象是否仍关联一个可 join 的线程,不等于线程此刻是否正在运行。
  • joinable 的 std::thread 析构会调用 std::terminate()
  • std::thread 不可复制,但可以移动。
  • 线程共享引用、指针或对象时,必须额外考虑对象生命周期和数据竞争。