在 C++ 中,线程的使用为我们带来了多任务并发执行的能力,但与此同时,如何管理和回收线程资源也是一个不可忽视的问题。虽然一个进程的所有线程共享同一进程的栈空间,但每个线程拥有自己独立的栈,用来保存其局部变量及函数调用的上下文。因此,在子线程运行完毕后,必须显式或隐式地回收其占用的资源,否则会导致资源泄露。


一、线程的资源回收方法

C++11 引入了 std::thread 类,并提供了两种主要的方式来管理线程结束后的资源回收:join()detach()。选择哪种方式取决于应用场景以及我们对线程结束时行为的预期。

1.1 join() 方法

join() 是一种 同步 方法,用于主线程等待子线程的执行结束。当调用 join() 时,主线程会阻塞,直到指定的子线程结束,随后回收该子线程的资源。如果子线程已提前结束,join() 会立即返回。

优点

  • 保证了主线程和子线程之间的同步执行顺序,主线程可以等待子线程的任务完成。
  • 可以确保回收已结束的子线程资源。

缺点

  • 如果子线程执行时间较长,主线程会被阻塞,影响主程序的效率。

1.2 detach() 方法

detach() 是一种 异步 方法,它将子线程与主线程分离,意味着子线程将自行运行,主线程不会等待子线程的完成。在调用 detach() 后,子线程独立运行,主线程不再管理它的状态。当子线程完成后,系统会自动回收其资源,分离后的子线程无法再被 join()

优点

  • 主线程不会阻塞,可以继续执行其他任务。
  • 子线程自动管理自己的生命周期,不需要主线程再干预。

缺点

  • 主线程无法确保子线程任务的完成,分离的子线程可能在主线程结束时仍未完成工作。

1.3 joinable() 方法

std::thread 中,还提供了 joinable() 方法,用于判断当前线程是否可被 join()。当线程已经被 detach() 或已经被 join() 后,joinable() 返回 false,否则返回 true


二、使用 join()detach() 的代码示例

下面的代码展示了如何使用 join()detach() 方法管理线程资源的回收:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>
#include <thread>                // 线程类头文件。
#include <unistd.h>
using namespace std;

// 定义一个普通函数作为线程的任务函数。
void func(int bh, const string& str) {
    for (int ii = 1; ii <= 10; ii++) {
        cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
        sleep(1);  // 模拟工作负载,休眠 1 秒。
    }
}

int main() {
    // 创建两个线程来执行普通函数 func。
    thread t1(func, 3, "我是一只傻傻鸟。");
    thread t2(func, 8, "我有一只小小鸟。");

    // 分离子线程,主线程不再管理它们的状态。
    t1.detach(); 
    t2.detach();

    // 让主线程睡眠 12 秒,以确保子线程有足够的时间完成任务。
    // 否则主线程提前结束,子线程会被强制终止。
    sleep(12);

    // 由于线程已经被分离,无法再调用 join()。
    // t1.join();  // 错误:分离后的线程无法再 join
    // t2.join();  // 错误:分离后的线程无法再 join

    return 0;
}

2.1 join() 示例

如果我们需要确保主线程等待子线程执行完毕后再继续执行主线程任务,我们可以使用 join() 方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#include <iostream>
#include <thread>
#include <unistd.h>
using namespace std;

void func(int bh, const string& str) {
    for (int ii = 1; ii <= 10; ii++) {
        cout << "第" << ii << "次表白:亲爱的" << bh << "号," << str << endl;
        Sleep(1);  // 模拟工作负载,休眠 1 秒。
    }
}

int main() {
    // 创建两个线程执行任务
    thread t1(func, 3, "我是一只傻傻鸟。");
    thread t2(func, 8, "我有一只小小鸟。");

    cout << "等待子线程完成...\n";

    // 使用 join 等待子线程执行完毕
    t1.join(); 
    t2.join();

    cout << "所有子线程完成,主线程结束。\n";
    return 0;
}

在这个例子中,t1.join()t2.join() 会确保主线程阻塞等待两个子线程结束,然后主线程才会继续执行后续代码。适合需要主线程依赖子线程任务结果的场景。


3. 何时使用 join()detach()

3.1 使用 join()

  • 当子线程的任务必须在主线程结束之前完成时使用,例如主线程依赖于子线程的结果或资源。
  • 当子线程的任务需要与主线程进行同步时,例如主线程需要等待子线程完成某些工作后再继续。

3.2 使用 detach()

  • 当子线程的任务可以独立运行,不需要主线程干预时使用。
  • 当希望主线程不等待子线程完成,并且让系统自动回收子线程资源时使用。
  • 适合后台执行的任务,如日志写入、定时任务等不需要主线程等待的情况。

四、总结

在 C++11 中,join()detach() 是两种处理线程资源回收的主要方法:

  • join() 确保主线程等待子线程结束,用于需要同步的场景;
  • detach() 分离子线程,使其与主线程无关,适合无需等待的异步场景。

此外,通过 joinable() 方法可以判断线程是否处于可 join() 的状态。