【C++11】线程创建于管理(std::thread)详解

目录

一、基本用法

1.1. 包含必要的头文件

1.2. 创建线程

1.3. 传递参数给线程函数

1.4. 使用 Lambda 表达式

1.5. 使用 std::ref 或 std::cref 传递引用

1.6. 线程的 ID

1.7. 检查线程是否可连接

1.8. 线程管理

1.8.1. 等待线程结束

1.8.2. 分离线程

1.8.3. 如果不调用 join() 或 detach()

二、高级特性

2.1. 线程同步

2.2. 线程本地存储(Thread Local Storage, TLS)

2.3. 线程中断

2.4. 线程安全和并发数据结构

2.5. 捕获列表和 Lambda 表达式

2.6. 交换和移动

三、注意事项

3.1. 线程生命周期管理

3.2. 线程同步与互斥

3.3. 线程参数传递

3.4. 异常处理

3.5. 性能考虑

3.6. 嵌入式和特定平台注意事项

四、使用场景总结

4.1. 并行处理耗时任务

4.2. 提高用户界面的响应性

4.3. 后台处理

4.4. 并发访问共享资源

4.5. 并发计算

4.6. 异步操作

4.7. 并发服务器

五、总结


std::thread 是 C++11 引入的一个类,是 C++11 标准库中的一个关键特性,它提供了一种在 C++ 程序中创建和管理线程的方法。通过使用 std::thread,可以很容易地在你的 C++ 程序中创建多线程应用程序。每个 std::thread 对象都代表了一个独立的执行线程,这些线程可以并行地执行不同的任务。

下面是对 std::thread 的详细介绍,包括其基本用法、高级特性以及需要注意的事项。

一、基本用法

使用 std::thread,可以并行地执行代码,从而利用多核处理器的优势来提高程序的性能。

以下是 std::thread 的一些基本用法。

1.1. 包含必要的头文件

要使用 std::thread,需要包含 <thread> 头文件。

#include <thread>  
#include <iostream>

1.2. 创建线程

要创建一个新的线程,需要提供一个可调用对象(如函数、lambda 表达式、函数对象、绑定表达式等)给 std::thread 的构造函数。这个可调用对象将在新的线程中执行。

示例 :使用函数指针

#include <iostream>  
#include <thread>  
  
void threadFunction() {  
    std::cout << "Hello from thread!" << std::endl;  
}  
  
int main() {  
    std::thread t(threadFunction);  
    t.join(); // 等待线程结束  
    return 0;  
}

在这个例子中,threadFunction 函数被一个新的线程执行。t.join() 调用确保主线程(即执行 main 函数的线程)会等待新创建的线程结束后再继续执行。

1.3. 传递参数给线程函数

可以像调用普通函数一样,向 std::thread 构造函数传递参数,这些参数会被拷贝或移动到新线程中。

示例 :传递参数给线程函数

#include <iostream>  
#include <thread>  
  
void threadFunction(int x, double y) {  
    std::cout << "Received: " << x << ", " << y << std::endl;  
}  
  
int main() {  
    std::thread t(threadFunction, 42, 3.14);  
    t.join();  
    return 0;  
}

1.4. 使用 Lambda 表达式

Lambda 表达式是定义匿名函数对象的简洁方式,非常适合与 std::thread 一起使用。

示例 :使用 Lambda 表达式

int main() {  
    std::thread t([]() {  
        std::cout << "Hello from lambda thread!" << std::endl;  
    });  
    t.join();  
    return 0;  
}

1.5. 使用 std::ref 或 std::cref 传递引用

当想在线程中修改外部变量时,需要传递引用而不是值。由于 std::thread 的构造函数默认按值捕获参数,所以需要使用 std::ref 或 std::cref 来传递引用。

#include <iostream>  
#include <thread>  
#include <functional> // 为了 std::ref  
  
void modifyValue(int& x) {  
    x = 42;  
}  
  
int main() {  
    int value = 0;  
    std::thread t(modifyValue, std::ref(value)); // 注意 std::ref 的使用  
    t.join();  
    std::cout << "Value is now: " << value << std::endl; // 输出 42  
    return 0;  
}

1.6. 线程的 ID

每个 std::thread 对象都有一个唯一的标识符,可以通过调用 get_id() 方法来获取。

std::thread t(threadFunction);  
std::cout << "Thread ID: " << t.get_id() << std::endl;  
t.join();

1.7. 检查线程是否可连接

可以通过 joinable() 成员函数检查一个 std::thread 对象是否代表了一个可运行的线程(即,它是否已被创建但尚未被 join() 或 detach())。

std::thread t(threadFunction);  
if (t.joinable()) {  
    t.join();  
}

1.8. 线程管理

1.8.1. 等待线程结束

  • 主线程可以通过调用 join() 方法来等待其他线程完成。如果不调用 join() 或 detach(),则当 std::thread 对象被销毁时,程序会调用 std::terminate()终止未结束的线程,这可能会导致资源泄露或其他问题。

下面是一个简单的示例,它创建了一个线程来打印一些消息,并在主线程中等待该线程完成。

#include <iostream>  
#include <thread>  
  
void threadFunction() {  
    for (int i = 0; i < 5; ++i) {  
        std::cout << "Thread is running, count: " << i << std::endl;  
        std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟耗时操作  
    }  
}  
  
int main() {  
    std::thread t(threadFunction); // 创建并启动线程  
  
    // 在这里可以做一些其他工作,但这里我们只是等待线程完成  
    t.join(); // 等待线程t完成  
  
    std::cout << "Thread has finished execution." << std::endl;  
  
    return 0;  
}

1.8.2. 分离线程

  • 默认情况下,新创建的线程会与创建它的线程(通常是主线程)同步。可以通过调用 detach() 方法来分离线程,这样主线程就可以继续执行,而不需要等待新线程结束。
  • 通过调用 detach() 方法,可以使线程独立于主线程运行。一旦线程被分离,就不能再次对其调用 join() 或 detach(),并且当主线程结束时,分离的线程将继续运行,直到完成其任务。

示例 :分离线程

#include <iostream>  
#include <thread>  
  
void threadFunction() {  
    // 假设这里有一些耗时的操作  
    std::cout << "Thread is running in the background." << std::endl;  
}  
  
int main() {  
    std::thread t(threadFunction);  
    t.detach(); // 分离线程,主线程继续执行  
    // 注意:分离后,主线程结束时不会等待这个线程完成  
    // 这里的程序可能在后台线程完成之前就结束了  
    // 在实际应用中,你可能需要其他机制来确保所有线程都已完成  
    return 0;  
}

注意:分离线程后,无法再与这个线程进行同步(如 join() 或 detach()),且当主线程结束时,如果分离线程还在运行,程序将立即终止,这可能导致资源泄露或其他问题。 

1.8.3. 如果不调用 join() 或 detach()

如果不调用 join() 或 detach(),并且 std::thread 对象在作用域结束时被销毁,程序将调用 std::terminate() 终止。这是因为 C++ 标准要求,如果 std::thread 的析构函数被调用,并且线程仍然是可连接的(即,没有被 join() 或 detach()),则必须调用 std::terminate()

下面是一个不安全的示例,它展示了这种情况:

#include <iostream>  
#include <thread>  
  
void threadFunction() {  
    // 假设这里有一些耗时的操作  
}  
  
int main() {  
    {  
        std::thread t(threadFunction); // 创建一个线程,但作用域仅限于这个块  
        // 注意:我们没有调用 t.join() 或 t.detach()  
        // 当这个块结束时,t 的析构函数将被调用,但线程仍在运行  
        // 这将导致 std::terminate() 被调用,从而终止程序  
    }  
  
    // 这段代码不会被执行,因为程序已经在上面的块结束时终止了  
    std::cout << "This line will not be executed." << std::endl;  
  
    return 0; // 永远不会到达这里  
}

为了避免这种情况,应该在 std::thread 对象被销毁之前调用 join() 或 detach()。如果知道线程何时会结束,通常使用 join() 是更安全的选择,因为它可以确保主线程等待子线程完成。如果您不关心子线程的完成时间,或者想要让子线程独立于主线程运行,那么可以使用 detach()。但是,请注意,使用 detach() 时需要小心管理线程的生命周期和资源共享。

二、高级特性

除了基本用法外, std::thread还有一些高级特性和使用场景。下面将介绍一些 std::thread 的高级特性。

2.1. 线程同步

虽然 std::thread 本身不直接提供同步机制,但可以使用其他同步原语(如 std::mutexstd::condition_variablestd::unique_lock 等)与 std::thread 一起使用来管理线程间的同步。

示例:使用 std::mutex 同步线程

#include <iostream>  
#include <thread>  
#include <mutex>  
  
std::mutex mtx; // 全局互斥锁  
  
void print_block(int n, char c) {  
    mtx.lock(); // 锁定互斥锁  
    for (int i = 0; i < n; ++i) { std::cout << c; }  
    std::cout << '\n';  
    mtx.unlock(); // 解锁互斥锁  
}  
  
int main() {  
    std::thread th1(print_block, 50, '*');  
    std::thread th2(print_block, 50, '$');  
  
    th1.join();  
    th2.join();  
  
    return 0;  
}

注意:在上面的示例中,虽然使用了 std::mutex 来同步线程,但这种方式可能导致死锁或不必要的性能开销。更常见的做法是使用 std::lock_guard 或 std::unique_lock 来自动管理锁的生命周期。

2.2. 线程本地存储(Thread Local Storage, TLS)

虽然 std::thread 本身不直接提供 TLS,但可以使用 thread_local 关键字来声明线程局部变量。

示例:使用 thread_local

#include <iostream>  
#include <thread>  
  
thread_local int tls_counter = 0; // 每个线程都有自己的 tls_counter 副本  
  
void increment_tls_counter() {  
    ++tls_counter;  
    std::cout << "TLS counter: " << tls_counter << std::endl;  
}  
  
int main() {  
    std::thread t1(increment_tls_counter);  
    std::thread t2(increment_tls_counter);  
  
    increment_tls_counter(); // 在主线程中调用  
  
    t1.join();  
    t2.join();  
  
    return 0;  
}

2.3. 线程中断

C++ 标准库中没有直接提供线程中断的机制(与 Java 中的 interrupt() 方法不同)。然而,可以通过共享状态或条件变量来实现类似的功能。

示例:使用共享状态中断线程

#include <iostream>  
#include <thread>  
#include <atomic>  
#include <chrono>  
  
std::atomic<bool> stop(false); // 原子变量,用于线程中断  
  
void do_work() {  
    while (!stop) {  
        // 执行一些工作  
        std::this_thread::sleep_for(std::chrono::milliseconds(100));  
    }  
    std::cout << "Work stopped\n";  
}  
  
int main() {  
    std::thread worker(do_work);  
  
    std::this_thread::sleep_for(std::chrono::seconds(2)); // 等待2秒  
    stop = true; // 设置停止标志  
  
    worker.join();  
  
    return 0;  
}

2.4. 线程安全和并发数据结构

虽然 std::thread 提供了创建和管理线程的能力,但需要自己确保数据在并发环境中的安全性。这通常涉及使用互斥锁、原子操作或并发数据结构(如 C++17 中的 std::shared_mutex)。

2.5. 捕获列表和 Lambda 表达式

当使用 Lambda 表达式与 std::thread 时,可以通过捕获列表来捕获外部变量。注意,默认情况下,捕获的变量是按值捕获的,这可能导致数据竞争或不一致的视图。

示例:使用 Lambda 表达式和捕获列表

#include <iostream>  
#include <thread>  
#include <vector>  
  
int main() {  
    std::vector<int> data = {1, 2, 3, 4, 5};  
  
    // 按值捕获 data  
    std::thread t1([&data]() {  
        for (int& val : data) {  
            val *= 2;  
        }  
    });  
  
    t1.join();  
  
    for (int val : data) {  
        std::cout << val << ' ';  
    }  
    std::cout << '\n';  
  
    return 0;  
}

注意:上面的 Lambda 表达式使用了 [&data] 来捕获 data 的引用,而不是值。这是为了确保线程能够修改原始数据。但是,这也引入了并发访问的风险,因此需要谨慎处理。

2.6. 交换和移动

std::thread 支持移动语义,但不支持拷贝语义。这意味着可以将一个 std::thread 对象的所有权转移到另一个对象,但不能直接复制一个 std::thread 对象。

std::thread t1(threadFunction);  
std::thread t2 = std::move(t1); // 移动t1到t2  
// 此时t1不再代表任何线程,且t1.joinable()将返回false

三、注意事项

使用 std::thread 时,需要注意以下几个方面以确保程序的正确性和稳定性。

3.1. 线程生命周期管理

  • join() 或 detach() 的调用:在 std::thread 对象销毁之前,必须确保它已经被 join() 或 detach()。如果线程仍然是可连接的(joinable)并且没有被适当处理,std::thread 的析构函数将调用 std::terminate() 来终止程序。
  • RAII 管理:推荐使用 RAII(资源获取即初始化)技术来管理 std::thread 的生命周期,通过将线程对象封装在类中并在析构函数中调用 join() 或 detach() 来确保资源被正确释放。

3.2. 线程同步与互斥

  • 避免数据竞争:当多个线程可能同时访问共享数据时,必须采取适当的同步措施,如使用互斥锁(std::mutex)、条件变量(std::condition_variable)等,以避免数据竞争和不一致性。
  • 死锁的预防:在设计多线程程序时,要注意避免死锁,即两个或多个线程相互等待对方持有的资源而无法继续执行的情况。

3.3. 线程参数传递

  • 临时对象的生命周期:当通过 std::thread 构造函数传递参数时,需要确保这些参数(特别是引用和指针)在线程函数执行期间保持有效。对于局部变量,最好通过值传递或将其封装在具有适当生命周期的对象中。
  • 引用和指针的传递:传递引用或指针时要特别小心,因为它们可能指向即将被销毁的局部变量的内存。在这种情况下,可以使用 std::ref 或 std::cref 来包装引用,以确保它们在线程中保持有效。

3.4. 异常处理

  • 线程内的异常处理:线程函数内部可能抛出异常,这些异常必须被捕获并适当处理,以防止它们传播到线程之外并导致未定义行为。
  • 主线程中的异常处理:当主线程等待线程完成(通过 join())时,如果线程函数抛出了异常且未被捕获,则这些异常将在线程被 join() 时重新抛出到主线程中。因此,主线程必须准备好处理这些异常。

3.5. 性能考虑

  • 线程创建和销毁的开销:线程的创建和销毁是有开销的,因此应避免频繁地创建和销毁线程。在可能的情况下,使用线程池来重用线程可以减少这些开销。
  • 负载均衡:在分配任务给线程时,要注意负载均衡,以避免某些线程过载而其他线程空闲的情况。

3.6. 嵌入式和特定平台注意事项

  • 堆栈大小:在嵌入式系统中,线程的堆栈大小可能需要手动设置,以避免堆栈溢出。
  • 线程优先级:根据应用需求,可能需要设置线程的优先级以优化系统性能。
  • 平台兼容性:不同平台对多线程的支持和限制可能不同,因此在跨平台开发时需要注意这些差异。

四、使用场景总结

以下是 std::thread 使用场景的一些总结。

4.1. 并行处理耗时任务

当程序中有多个耗时的任务需要执行,并且这些任务之间没有明显的依赖关系时,可以使用 std::thread 来并行处理这些任务。这可以显著减少程序的总运行时间,特别是当这些任务可以在多核处理器上并行执行时。

4.2. 提高用户界面的响应性

在图形用户界面(GUI)程序中,长时间运行的任务(如文件读写、数据处理等)可能会阻塞主线程,导致用户界面无响应。通过将耗时任务移至 std::thread 中执行,可以保持主线程(通常负责处理用户输入和更新界面)的响应性。

4.3. 后台处理

对于需要在后台执行的任务(如日志记录、定时任务、网络请求等),可以使用 std::thread 来创建一个或多个线程,这些线程在后台运行,不会干扰主线程的执行。

4.4. 并发访问共享资源

在并发编程中,多个线程可能需要同时访问共享资源(如全局变量、文件、数据库等)。使用 std::thread 时,需要谨慎处理线程同步问题,以避免数据竞争和死锁等并发问题。这通常涉及使用互斥锁(std::mutex)、条件变量(std::condition_variable)等同步机制。

4.5. 并发计算

对于需要进行大量计算的应用程序(如科学计算、图像处理、视频编码等),可以利用 std::thread 来实现并发计算。通过将计算任务分配给多个线程,可以充分利用多核处理器的计算能力,从而加速计算过程。

4.6. 异步操作

在某些情况下,程序可能需要执行一些异步操作(如等待用户输入、等待网络响应等)。使用 std::thread 可以创建一个线程来执行这些异步操作,并通过某种机制(如共享变量、回调函数、条件变量等)将结果通知给主线程。

4.7. 并发服务器

在开发并发服务器时,std::thread 可以用于处理来自多个客户端的请求。每个客户端请求可以由一个单独的线程来处理,从而实现高效的并发处理。然而,在实际应用中,更常见的做法是使用线程池(std::thread 的一个高级应用)来管理线程,以避免创建和销毁线程的开销。

五、总结

std::thread 是 C++ 标准库中处理多线程的一个非常强大的工具,但它也要求程序员对多线程编程的复杂性有一定的了解,包括线程同步、数据竞争和死锁等问题。

5.1. std::thread 的常用成员函数汇总

std::thread 的常用成员函数及其功能概述汇总成的表格:

成员函数功能概述注意/用途
joinable()检查线程对象是否可被 join。如果线程正在执行或可执行且未被 join 或 detach,则返回 true;否则返回 false。不带参构造的 std::thread 对象或已被移动的 std::thread 对象不可 join。
join()阻塞当前线程,直到被 join 的线程完成其执行。如果线程未启动或已被 join/detach,则行为未定义(通常抛出异常)。确保子线程在主线程继续执行之前完成其任务。
detach()将线程与 std::thread 对象分离,允许线程在后台继续运行。分离后,线程不再与任何 std::thread 对象关联,且不能被 join。一旦线程被 detach,就无法再通过 std::thread 对象控制或等待该线程。
get_id()获取线程的标识符(ID),类型为 std::thread::id。该 ID 在线程生命周期内唯一,但结束后可能会被重用。用于标识和区分不同的线程。
native_handle()获取与实现相关的本机线程句柄。具体行为和返回值取决于操作系统和 C++ 运行时库。使用时需了解当前平台的线程 API,并谨慎处理句柄以避免资源泄露或安全问题。
hardware_concurrency()返回硬件支持的并发线程数量的估计值(静态成员函数)。此值是一个提示,表示系统可能支持的并行线程数,但并非绝对限制。帮助开发者在创建线程时决定合适的线程数量。
swap()交换两个 std::thread 对象的状态。如果两个对象都表示活动的线程,则它们的执行不受影响;但如果一个对象是空的,则另一个对象将不再与任何线程关联。在需要转移线程所有权或管理线程集合时很有用。

std::thread 类在 C++11 及以后的版本中用于表示一个线程。关于 std::thread 的构造函数和析构函数,这里有一个简要的概述:

5.2. std::thread 类构造函数

std::thread 类提供了几个构造函数,但最常用的是以下两个。

1. 默认构造函数

std::thread() noexcept;
  • 创建一个空的 std::thread 对象,它不表示任何线程。这种类型的对象是不可 join 的,也不可被 detach。

2. 带可调用对象的构造函数

template< class Function, class... Args >  
explicit thread( Function&& f, Args&&... args );
  • 创建一个新线程,该线程将执行给定的可调用对象 ff 可以是函数、lambda 表达式、绑定表达式或其他可调用对象。args... 是传递给 f 的参数。这个构造函数启动一个新线程,并立即返回,允许 std::thread 对象被用于管理新创建的线程。

5.3. std::thread 类析构函数

std::thread 的析构函数在对象被销毁时自动调用,它执行以下操作:

~thread();
  • 如果 std::thread 对象表示一个可 join 的线程(即该线程仍在执行且尚未被 join 或 detach),则析构函数会调用 std::terminate(),以终止程序。这是为了防止程序在 std::thread 对象被销毁时丢失对线程的跟踪,从而可能导致资源泄露或程序状态的不一致。
  • 如果 std::thread 对象表示一个已经 join 或 detach 的线程,或者是一个空的 std::thread 对象(即不表示任何线程),则析构函数不会执行任何操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值