首页 > 编程笔记 > C++笔记 阅读:19

C++ std::thread多线程编程的用法(附带实例)

线程是可以由调度程序(如操作系统)独立管理的指令序列,线程可以是软件或硬件。

软件线程是由操作系统管理的执行线程,它们可以在单个处理单元上运行,通常是通过时间片进行调度的。这是一种调度机制,在操作系统调度另一个软件线程在同一个处理单元上运行之前,每个线程在处理单元上获得一个执行时间段(以毫秒为单位)。

硬件线程是物理级别的执行线程,它们基本上就是 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 类表示单个执行线程,它有如下几个构造函数:
在这种情况下,线程函数不能返回值,函数实际具有除 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 的新线程类,解决了这一不便。

相关文章