C++ std::thread多线程编程的用法(附带实例)
线程是可以由调度程序(如操作系统)独立管理的指令序列,线程可以是软件或硬件。
软件线程是由操作系统管理的执行线程,它们可以在单个处理单元上运行,通常是通过时间片进行调度的。这是一种调度机制,在操作系统调度另一个软件线程在同一个处理单元上运行之前,每个线程在处理单元上获得一个执行时间段(以毫秒为单位)。
硬件线程是物理级别的执行线程,它们基本上就是 CPU 或 CPU 核心。它们可以同时运行,也就是说,在具有多处理器或多核的系统上并行运行。许多软件线程可以在硬件线程上并发运行,通常基于时间片的调度方式。
C++ 标准库为软件线程提供了支持。在本节中,我们将学习创建线程和与线程相关的其他操作。
在以下示例中,声明了 print_time() 函数,此函数将本地时间输出到控制台,实现如下:
1) 如果想在创建新线程的时候不启动线程(即不执行线程),那么可以使用线程的默认构造函数:
2) 通过把一个函数传递给 std::thread 的构造函数来创建一个 std::thread 对象,即可在新创建的线程上执行这个函数:
3) 在另一个线程上执行一个带参数的函数,其方法是构造一个 std::thread 对象,先把这个待执行的函数传给 std::thread 构造函数,然后再传入函数的参数:
4) 使用 join() 方法可以等待一个线程执行完成:
5) 可以使用 detach() 方法来允许线程独立于当前线程执行。意思就是调用 detach() 方法后,这个线程不再被当前线程(译者注:创建这个线程的线程)所管理了,即独立运行直到这个线程结束:
6) 以引用的形式把参数传递给函数时,可以使用 std::ref 或 std::cref(常量引用)包装:
7) 要在指定的运行时间停止线程的执行,可以使用 std::this_thread::sleep_for() 函数:
8) 要在指定的某个时间点停止线程的执行,可以使用 std::this_thread::sleep_until() 函数:
9) 要暂停当前线程的执行并为另一个线程提供执行的机会,请使用 std::this_thread::yield():
在这种情况下,线程函数不能返回值,函数实际具有除 void 以外的返回类型并不违法,但它会忽略函数直接返回的任何值。如果必须返回值,可以使用共享变量或函数参数。
如果函数以异常终止,则无法通过捕获异常 try...catch 语句,该语句位于线程启动且程序通过调用 std::terminate() 异常终止的上下文中。所有异常都必须在执行线程中捕获,但它们可以通过 std::exception_ptr 对象跨线程传输。
线程开始执行后,它既可以连接也可以分离:
连接线程是通过 join() 完成的,分离线程是通过 detach() 完成的。一旦调用这两个方法中的任何一个,线程就被认为是不可连接的,线程对象可以被安全地销毁。当线程被分离时,它可能需要访问的共享数据必须在整个执行过程中可用。joinable()方法指示线程是否可以连接。
每个线程都有一个可以检索的标识符,对于当前线程,调用 std::this_thread::get_id() 函数,对于由 thread 对象表示的另一个执行线程,调用其 get_id() 方法。
std::this_thread 命名空间中有几个额外的实用程序函数:
std::thread 类要求显式调用 join() 方法来等待线程执行完成,这可能会导致编程错误。C++20 标准提供了一个名为 std::jthread 的新线程类,解决了这一不便。
软件线程是由操作系统管理的执行线程,它们可以在单个处理单元上运行,通常是通过时间片进行调度的。这是一种调度机制,在操作系统调度另一个软件线程在同一个处理单元上运行之前,每个线程在处理单元上获得一个执行时间段(以毫秒为单位)。
硬件线程是物理级别的执行线程,它们基本上就是 CPU 或 CPU 核心。它们可以同时运行,也就是说,在具有多处理器或多核的系统上并行运行。许多软件线程可以在硬件线程上并发运行,通常基于时间片的调度方式。
C++ 标准库为软件线程提供了支持。在本节中,我们将学习创建线程和与线程相关的其他操作。
C++ std::thread类
线程的创建与执行通过 thread 类实现,在<thread>
头文件中声明,位于 std 命名空间。其他与线程相关的功能也在 <thread>
头文件中声明,但位于 std::this_thread 命名空间。在以下示例中,声明了 print_time() 函数,此函数将本地时间输出到控制台,实现如下:
inline void print_time() { auto now = std::chrono::system_clock::now(); auto stime = std::chrono::system_clock::to_time_t(now); auto ltime = std::localtime(&stime); std::cout << std::put_time(ltime, "%c") << '\n'; }接下来我们将看到如何使用线程执行常见操作。
C++管理线程的方式
使用以下解决方案来管理线程:1) 如果想在创建新线程的时候不启动线程(即不执行线程),那么可以使用线程的默认构造函数:
std::thread t;
2) 通过把一个函数传递给 std::thread 的构造函数来创建一个 std::thread 对象,即可在新创建的线程上执行这个函数:
void func1() { std::cout << "thread func without params" << '\n'; } std::thread t(func1); std::thread t([]() { std::cout << "thread func without params" << '\n'; });
3) 在另一个线程上执行一个带参数的函数,其方法是构造一个 std::thread 对象,先把这个待执行的函数传给 std::thread 构造函数,然后再传入函数的参数:
void func2(int const i, double const d, std::string const s) { std::cout << i << ", " << d << ", " << s << '\n'; } std::thread t(func2, 42, 42.0, "42");
4) 使用 join() 方法可以等待一个线程执行完成:
t.join();
5) 可以使用 detach() 方法来允许线程独立于当前线程执行。意思就是调用 detach() 方法后,这个线程不再被当前线程(译者注:创建这个线程的线程)所管理了,即独立运行直到这个线程结束:
t.detach();
6) 以引用的形式把参数传递给函数时,可以使用 std::ref 或 std::cref(常量引用)包装:
void func3(int & i) { i *= 2; } int n = 42; std::thread t(func3, std::ref(n)); t.join(); std::cout << n << '\n'; // 84
7) 要在指定的运行时间停止线程的执行,可以使用 std::this_thread::sleep_for() 函数:
void func4() { using namespace std::chrono; print_time(); std::this_thread::sleep_for(2s); print_time(); } std::thread t(func4); t.join();
8) 要在指定的某个时间点停止线程的执行,可以使用 std::this_thread::sleep_until() 函数:
void func5() { using namespace std::chrono; print_time(); std::this_thread::sleep_until( std::chrono::system_clock::now() + 2s); print_time(); } std::thread t(func5); t.join();
9) 要暂停当前线程的执行并为另一个线程提供执行的机会,请使用 std::this_thread::yield():
void func6(std::chrono::seconds timeout) { auto now = std::chrono::system_clock::now(); auto then = now + timeout; do { std::this_thread::yield(); } while (std::chrono::system_clock::now() < then); } std::thread t(func6, std::chrono::seconds(2)); t.join(); print_time();
深度剖析std::thread
std::thread 类表示单个执行线程,它有如下几个构造函数:- 默认构造函数,只创建线程对象,不启动新线程的执行;
- 移动构造函数,它创建一个新的线程对象来执行一个线程,该线程从以前的对象创建。构造新对象后,另一个对象不再与线程相关联;
- 可变参数构造函数,第一个参数是线程的入口函数,其他的参数是要传递给线程函数的参数。参数需要通过值传递给线程函数,如果线程函数以引用或常量引用接受参数,则它们必须包装在 std::ref 或 std::cref 中,这些是生成 std::reference_wrapper 类型的对象的助手函数模板,它将引用封装在可复制和可分配的对象中。
在这种情况下,线程函数不能返回值,函数实际具有除 void 以外的返回类型并不违法,但它会忽略函数直接返回的任何值。如果必须返回值,可以使用共享变量或函数参数。
如果函数以异常终止,则无法通过捕获异常 try...catch 语句,该语句位于线程启动且程序通过调用 std::terminate() 异常终止的上下文中。所有异常都必须在执行线程中捕获,但它们可以通过 std::exception_ptr 对象跨线程传输。
线程开始执行后,它既可以连接也可以分离:
- 连接线程意味着阻止当前线程的执行,直到连接的线程结束其执行;
- 分离线程意味着将线程对象与它所代表的执行线程分离,允许同时执行当前线程和分离的线程。
连接线程是通过 join() 完成的,分离线程是通过 detach() 完成的。一旦调用这两个方法中的任何一个,线程就被认为是不可连接的,线程对象可以被安全地销毁。当线程被分离时,它可能需要访问的共享数据必须在整个执行过程中可用。joinable()方法指示线程是否可以连接。
每个线程都有一个可以检索的标识符,对于当前线程,调用 std::this_thread::get_id() 函数,对于由 thread 对象表示的另一个执行线程,调用其 get_id() 方法。
std::this_thread 命名空间中有几个额外的实用程序函数:
- yield() 方法提示调度程序激活另一个线程,这在实现繁忙等待例程时非常有用;
- sleep_for() 方法至少在指定的时间段内,阻止当前线程的执行(由于调度,线程进入睡眠状态的实际时间可能比请求的时间长);
- sleep_until() 方法会阻止当前线程的执行,直到至少达到指定的时间点(由于调度,睡眠的实际持续时间可能比请求的时间长)。
std::thread 类要求显式调用 join() 方法来等待线程执行完成,这可能会导致编程错误。C++20 标准提供了一个名为 std::jthread 的新线程类,解决了这一不便。