线程3.1
线程3.1
本节主要内容:
- 我们比较了c++11出现的线程和Linux的原生线程库,分析了c++11出现线程概念的必要性,以及和原生线程库在使用上的区别。我们自己也尝试做了一个简单封装,了解了大概是一个怎么样的思路。
- 我们比较重点的讲解了线程分离这个概念。我们分别讲了使用c++11的线程分离和使用原生线程库的线程分离。了解了线程分离的本质是:改变线程结束时的资源回收策略,而不是改变线程与进程的隶属关系。
- 对比了两种线程分离的资源回收策略的不同。毕竟C++11是原生线程库的封装,除了原生线程库本应该有的作用和特点,肯定是会带有一些C++11的特性在的。比如RAII什么的。
番外链接:
有关于c++中线程类的介绍都在这:
C++ thread类-CSDN博客
本文涉及到的线程库函数的详细说明在:
- POSIX线程库–罗列核心函数-CSDN博客
- POSIX线程库–详细函数说明2.1-CSDN博客
完整系列:
线程1.1-CSDN博客
线程1.2-CSDN博客
线程2.0-CSDN博客
C++11线程与Linux原生线程库的比较
1. 核心区别分析
1.1 抽象层次差异
- C++11线程:语言级别的线程抽象,提供跨平台的一致性接口
- Linux原生线程(pthread):操作系统级别的线程实现,直接调用系统调用
1.2 可移植性对比
// C++11线程 - 跨平台
#include <thread>
std::thread t(func);// Linux pthread - 平台相关
#include <pthread.h>
pthread_t thread;
pthread_create(&thread, NULL, func, NULL);
1.3 资源管理方式
- C++11:基于RAII原则,自动管理线程生命周期
- pthread:需要手动管理线程的创建、终止和资源释放
2. C++11多线程编程示例
2.1 基本线程创建与管理
#include <iostream>
#include <thread>
#include <vector>void simple_worker(int id) {std::cout << "线程 " << id << " 开始执行" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "线程 " << id << " 执行完成" << std::endl;
}int main() {std::vector<std::thread> threads;// 创建多个线程for (int i = 0; i < 5; ++i) {threads.emplace_back(simple_worker, i);}// 等待所有线程完成for (auto& t : threads) {t.join();}std::cout << "所有线程执行完毕" << std::endl;return 0;
}
2.2 互斥锁保护共享数据
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>std::mutex g_mutex;
int shared_counter = 0;void counter_worker(int increments) {for (int i = 0; i < increments; ++i) {std::lock_guard<std::mutex> lock(g_mutex);++shared_counter;}
}int main() {std::vector<std::thread> threads;// 创建4个线程,每个增加250次for (int i = 0; i < 4; ++i) {threads.emplace_back(counter_worker, 250);}for (auto& t : threads) {t.join();}std::cout << "最终计数器值: " << shared_counter << std::endl;return 0;
}
2.3 使用条件变量进行线程同步
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <queue>std::mutex mtx;
std::condition_variable cv;
std::queue<int> data_queue;
bool finished = false;void producer() {for (int i = 0; i < 10; ++i) {{std::lock_guard<std::mutex> lock(mtx);data_queue.push(i);std::cout << "生产数据: " << i << std::endl;}cv.notify_one();std::this_thread::sleep_for(std::chrono::milliseconds(100));}{std::lock_guard<std::mutex> lock(mtx);finished = true;}cv.notify_all();
}void consumer() {while (true) {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, []{ return !data_queue.empty() || finished; });while (!data_queue.empty()) {int data = data_queue.front();data_queue.pop();std::cout << "消费数据: " << data << std::endl;}if (finished && data_queue.empty()) {break;}}
}int main() {std::thread producer_thread(producer);std::thread consumer_thread(consumer);producer_thread.join();consumer_thread.join();std::cout << "生产消费完成" << std::endl;return 0;
}
2.4 异步任务处理
#include <iostream>
#include <future>
#include <vector>
#include <numeric>int compute_sum(const std::vector<int>& data) {std::cout << "异步计算开始..." << std::endl;int sum = std::accumulate(data.begin(), data.end(), 0);std::this_thread::sleep_for(std::chrono::seconds(2));return sum;
}int main() {std::vector<int> data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};// 异步执行计算任务std::future<int> result = std::async(std::launch::async, compute_sum, data);std::cout << "主线程继续执行其他工作..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "主线程工作完成,等待异步结果..." << std::endl;// 获取异步结果(会阻塞直到计算完成)int sum = result.get();std::cout << "计算结果: " << sum << std::endl;return 0;
}
3. 深入对比分析
3.1 性能特性
- C++11线程:在Linux上通常是对pthread的封装,有轻微性能开销
- pthread:直接系统调用,性能最优,但编码复杂度高
3.2 错误处理机制
- C++11:使用异常机制,更符合C++编程风格
- pthread:返回错误码,需要手动检查
3.3 特性支持
- C++11:提供更高级的抽象(future/promise、async等)
- pthread:提供更底层的控制(线程属性、取消等)
4. 选择建议
- 新项目:优先使用C++11线程,保证可移植性和现代C++特性
- 性能关键:在需要极致性能时考虑pthread
- 跨平台需求:必须使用C++11线程
- Linux专有特性:如需要特定Linux线程特性,使用pthread
5. 简单总结
简单来说,c++中的线程其实就是对POSIX线程库中的函数的一个封装。我们可以使用下面这个例子来简单证明一下:
# 大家注意了,我们编译链接的指令中,没有使用-lpthread
test_thread:testThread.ccg++ -o $@ $^ .PHONY:clean
clean:rm -f test_thread
代码依旧使用:
#include <iostream>
#include <thread>
#include <vector>void simple_worker(int id) {std::cout << "线程 " << id << " 开始执行" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << "线程 " << id << " 执行完成" << std::endl;
}int main() {std::vector<std::thread> threads;// 创建多个线程for (int i = 0; i < 5; ++i) {threads.emplace_back(simple_worker, i);}// 等待所有线程完成for (auto& t : threads) {t.join();}std::cout << "所有线程执行完毕" << std::endl;return 0;
}
当我们使用编译代码的时候就会发现:
lx@emperor:~/working/d47/threadtest$ make
g++ -o test_thread testThread.cc
/usr/bin/ld: /tmp/ccCkZ7ws.o: in function `std::thread::thread<void (&)(int), int&, void>(void (&)(int), int&)':
testThread.cc:(.text._ZNSt6threadC2IRFviEJRiEvEEOT_DpOT0_[_ZNSt6threadC5IRFviEJRiEvEEOT_DpOT0_]+0x37): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
make: *** [Makefile:2: test_thread] Error 1
这其中呢最明显的,大家都能看明白的一个东西就是:
undefined reference to `pthread_create'
pthread_create是POSIX线程库中的函数,为什么使用c++的thread类的时候会出现这样一个报错的?
答:很明显就是因为c++是对POSIX线程库的一个封装嘛,所以c++中的thread类的实现的底层就是POSIX线程库中的函数接口。
所以当我们编译链接的时候,没有声明链接pthread线程库,那么C++中的thread类就没用了。所以我们在使用c++中的thread类的时候,编译链接中的选项依旧要带有-lpthread。
其次就是c++为什么要对pthread库封装呢?这个问题的答案主要就是:实现语言的跨平台性。防止在Linux中写的代码在Windows中用不了。这就是c++封装pthread库的一个主要作用。
同理,其他语言如果也携带有线程库,那么大概率也都是封装的各个平台提供的线程库。
6. 简单复刻C++对pthread库的封装
Makefile
test_thread:testThread.ccg++ -o $@ $^ -lpthread -std=c++11.PHONY:clean
clean:rm -f test_thread
第一版:使用函数对象
Thread.hpp
#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T&)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:void Excute(){_func(_data);}public:Thread(func_t<T> func, T data, const std::string &name="none-name"): _func(func), _data(data), _threadname(name), _stop(true){}// 类成员函数,形参是有this指针的!!// 所以我们写成静态成员函数static void *threadroutine(void *args) {Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;}bool Start(){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}}void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data; func_t<T> _func;bool _stop;};
} // namespace ThreadModule#endif
template<typename T>
using func_t = std::function<void(T&)>;
这行代码的意思是:
- 定义一个模板别名
func_t - 对于任何类型
T,func_t<T>等价于std::function<void(T&)>
我们用一个类将线程封装起来,其中私有成员变量有用户态的线程TID,可以有线程名称,必须得有函数对象func_t<T> _func;。此外,我们还可以设置一个用来标记线程运行状态的标识符bool _stop;
Thread(func_t<T> func, T data, const std::string &name="none-name"): _func(func), _data(data), _threadname(name), _stop(true){}
使用构造函数来初始化线程。
这里的重点内容是:
public:
void Excute()
{_func(_data);
}
// 类成员函数,形参是有this指针的!!
// 所以我们写成静态成员函数
static void *threadroutine(void *args)
{Thread<T> *self = static_cast<Thread<T> *>(args);self->Excute();return nullptr;
}
bool Start()
{int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n){_stop = false;return true;}else{return false;}
}
为什么我们不直接封装pthread_create,而是要用一个静态成员函数来绕一下路?
理由:
第一点:我们封装线程类的时候,使用的是函数对象func_t<T> _func;,而不是函数指针,所以我们不能直接将函数对象作为pthread_create的参数传过去。所以我们必须得写一个中间函数,在代码中,这个中间函数就是threadroutine。我们用这个中间函数来使用函数对象。然后将threadroutine作为pthread_create函数的参数传过去。
第二点:由于pthread_create函数的参数要求:线程函数必须符合此签名:void* function_name(void* arg)。参数和返回值都是void*的线程函数才能作为参数。
但是类成员函数中都会有一个默认参数this指针。这就会导致类成员函数的签名不符合要求(函数参数不是void*,或者说,多了一个函数参数)。
所以这个中间函数threadroutine,就不能是类成员函数。为了解决这个问题,我们给它设置为了静态成员函数,这样它的函数参数中就不会有this指针了。从而符合pthread_create函数对参数的要求。
但是:
因为threadroutine没有this指针,所以这个中间函数就无法直接访问类内私有成员(函数对象)。
所以:
为了解决这个问题,我们使用pthread_create函数的时候,要把this指针传过去,这样就能访问类内的成员了。
上面的代码,我们不是将创建一个线程对象和启动线程分开写的嘛,我们也可以将这个过程合并在一起,我们可以这样写:
#ifndef __THREAD_HPP__
#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{template<typename T>using func_t = std::function<void(T&)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:// void Excute()// {// _func(_data);// }public:Thread(func_t<T> func, T data, const std::string &name="none-name"): _func(func), _data(data), _threadname(name), _stop(true){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n)_stop = false;}static void *threadroutine(void *args) {Thread<T> *self = static_cast<Thread<T> *>(args);//self->Excute(); ↓self->_func(self->_data);return nullptr;}// bool Start()// {// int n = pthread_create(&_tid, nullptr, threadroutine, this);// if(!n)// {// _stop = false;// return true;// }// else// {// return false;// }// }void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;func_t<T> _func;bool _stop;};
} // namespace ThreadModule#endif
相应的:testThread.cc
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;void print(int& cnt)
{while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}
}const int num = 10;int main()
{std::vector<Thread<int> > threads;// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, 10, name);}// // 2. 启动 一批线程// for (auto &thread : threads)// {// thread.Start();// }// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}return 0;
}
我们来看看都变了什么:
public:// void Excute()// {// _func(_data);// }public:Thread(func_t<T> func, T data, const std::string &name="none-name"): _func(func), _data(data), _threadname(name), _stop(true){int n = pthread_create(&_tid, nullptr, threadroutine, this);if(!n)_stop = false;}static void *threadroutine(void *args) {Thread<T> *self = static_cast<Thread<T> *>(args);//self->Excute(); ↓self->_func(self->_data);return nullptr;}// bool Start()// {// int n = pthread_create(&_tid, nullptr, threadroutine, this);// if(!n)// {// _stop = false;// return true;// }// else// {// return false;// }// }
我们在构造函数初始化完类内私有成员变量后,我们直接调用pthread_create,相当于把Start省略掉了。然后threadroutine也有点变化,我们其实可以在这个函数中直接使用函数对象。
不过有一点我们需要注意一下:
第一:静态成员函数不能直接访问类内成员,因为它没有this指针。
但是:我们传过去了一个this指针,所以静态成员函数就可以通过这个传过来的this指针(在函数中其实也就是self指针嘛),访问类内的成员(包括私有成员)。
那么我们到底需要注意什么呢?
一开始在改代码的时候,我写成了:self->_func(_data);。看明白没?
在使用函数对象的时候,我使用self指针,但是,我使用类内私有成员变量_data的时候,忘记了。所以编译就报错了。
所以大家需要注意一下,有时候就是这些不起眼的地方,容易出错。
应该写成这样:self->_func(self->_data);就对了。
testThread.cc
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;void print(int& cnt)
{while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}
}const int num = 10;int main()
{std::vector<Thread<int> > threads;// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, 10, name);}// 2. 启动 一批线程for (auto &thread : threads){thread.Start();}// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}return 0;
}
第二版:使用函数指针
Thread.hpp
//#ifndef __THREAD_HPP__
//#define __THREAD_HPP__#include <iostream>
#include <string>
#include <unistd.h>
#include <functional>
#include <pthread.h>namespace ThreadModule
{// 定义函数指针类型// 函数指针类型名是:worktypedef void*(* work_t)(void*);template<typename T>using func_t = std::function<void(T&)>;// typedef std::function<void(const T&)> func_t;template<typename T>class Thread{public:// void Excute()// {// _func(_data);// }public:Thread(work_t work, T data, const std::string &name="none-name"): _work(work), _data(data), _threadname(name), _stop(true){int n = pthread_create(&_tid, nullptr, _work, &_data);if(!n)_stop = false;}// static void *threadroutine(void *args) // 类成员函数,形参是有this指针的!!// {// Thread<T> *self = static_cast<Thread<T> *>(args);// //self->Excute(); ↓// self->_func(self->_data);// return nullptr;// }// bool Start()// {// int n = pthread_create(&_tid, nullptr, threadroutine, this);// if(!n)// {// _stop = false;// return true;// }// else// {// return false;// }// }void Detach(){if(!_stop){pthread_detach(_tid);}}void Join(){if(!_stop){pthread_join(_tid, nullptr);}}std::string name(){return _threadname;}void Stop(){_stop = true;}~Thread() {}private:pthread_t _tid;std::string _threadname;T _data;//func_t<T> _func;work_t _work;bool _stop;};
} // namespace ThreadModule//#endif
testThread.cc
#include <iostream>
#include <vector>
#include "Thread.hpp"using namespace ThreadModule;void* print(void* arg)
{int cnt = *(int*)arg;while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}return nullptr;
}const int num = 10;int main()
{std::vector<Thread<int> > threads;// 1. 创建一批线程for (int i = 0; i < num; i++){std::string name = "thread-" + std::to_string(i + 1);threads.emplace_back(print, 10, name);}// // 2. 启动 一批线程// for (auto &thread : threads)// {// thread.Start();// }// 3. 等待一批线程for (auto &thread : threads){thread.Join();std::cout << "wait thread done, thread is: " << thread.name() << std::endl;}return 0;
}
主要变化:
这一次我们封装线程类,类内的私有成员从函数对象func_t<T> _func;变成了函数指针work_t _work;。
但是吧,我感觉使用函数指针来封装,方便的是封装的人,而不是用户。感觉就是封装了和没封装没啥区别。
封装后该怎么写还是怎么写,还是和封装之前一样,没有让使用变得更简洁简单。
就那那个print函数来说吧。
第一版封装我可以按照正常函数,按照我既定的逻辑去写:
void print(int& cnt)
{while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}
}
这个函数的参数和返回值,我想它是怎么样的就是怎么样的。
但是当我们使用函数指针来封装的时候:
void* print(void* arg)
{int cnt = *(int*)arg;while (cnt){std::cout << "hello I am myself thread, cnt: " << cnt-- << std::endl;sleep(1);}return nullptr;
}
就必须按照要求写了,返回值和参数必须是void*,所以这种封装对于用户来说其实是挺多余的。没啥用。
当然了,我们这里只是做学习用途,学学看,了解了解,总没坏处。
C++11线程分离
1. 线程分离的基本概念
1.1 什么是线程分离
线程分离是一种线程管理机制,它改变了线程结束时的资源回收方式。当一个线程被设置为分离状态后,该线程结束时系统会自动回收其资源,而不需要其他线程调用join来等待其结束。
1.2 分离线程的核心特性
#include <iostream>
#include <thread>
#include <chrono>void detached_worker() {std::cout << "分离线程开始工作..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(3));std::cout << "分离线程工作完成" << std::endl;
}int main() {std::thread t(detached_worker);// 将线程设置为分离状态t.detach();std::cout << "主线程继续执行,不等待分离线程" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));// 注意:分离后不能再调用join()// t.join(); // 错误!会导致程序崩溃return 0;
}
2. 关键问题深度分析
2.1 分离线程后的现象观察
实验代码:观察分离线程的行为
#include <iostream>
#include <thread>
#include <chrono>void long_running_task(int id) {for (int i = 0; i < 5; ++i) {std::cout << "线程" << id << "执行第" << i << "步" << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));}std::cout << "线程" << id << "正常结束" << std::endl;
}int main() {std::thread t1(long_running_task, 1);std::thread t2(long_running_task, 2);// 分离第一个线程,等待第二个线程t1.detach();t2.join(); // 主线程等待t2结束std::cout << "主线程退出" << std::endl;return 0;
}
观察结果分析:
- 线程t1被分离后,主线程不会等待它完成
- 线程t2需要join,主线程会等待t2完成
- 主线程退出时,如果分离线程还在运行,会被强制终止
2.2 进程退出对分离线程的影响
验证代码:进程退出时分离线程的命运
#include <iostream>
#include <thread>
#include <chrono>void infinite_worker() {int count = 0;while (true) {std::cout << "分离线程运行中...计数: " << ++count << std::endl;std::this_thread::sleep_for(std::chrono::seconds(1));if (count >= 5) {std::cout << "分离线程完成5次循环" << std::endl;break;}}
}int main() {std::thread t(infinite_worker);t.detach();std::cout << "主线程快速退出,观察分离线程是否继续" << std::endl;// 主线程立即退出,不等待分离线程return 0;
}
运行结果:
lx@emperor:~/working/d47/threadtest$ ./test_thread
主线程快速退出,观察分离线程是否继续分离线程运行中...计数: lx@emperor:~/working/d47/threadtest$ ./test_thread
主线程快速退出,观察分离线程是否继续
分离线程运行中...计数: 1lx@emperor:~/working/d47/threadtest$ ./test_thread
主线程快速退出,观察分离线程是否继续分离线程运行中...计数:
关键发现:
- 进程是所有线程的容器,进程退出意味着所有线程终止
- 分离线程并不会获得"独立性",仍然受进程生命周期约束
- 这是操作系统的进程管理机制决定的,不是线程库的特性
3. 线程分离的正确理解
3.1 分离的本质是什么
线程分离的本质是改变线程结束时的资源回收策略,而不是改变线程与进程的隶属关系。
资源管理对比:
// 非分离线程(需要手动join)
std::thread t(worker);
// ... 其他操作
t.join(); // 必须调用,否则资源泄漏// 分离线程(自动回收)
std::thread t(worker);
t.detach(); // 分离后,线程结束自动回收资源
// 不需要也不能调用join
3.2 共享资源的所有权关系
即使线程被分离,它仍然:
- 共享进程的地址空间
- 访问相同的内存区域
- 使用相同的文件描述符
- 受相同的进程权限限制
4. 线程分离的实际应用场景
4.1 服务器编程中的典型应用
后台任务处理示例:
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#include <mutex>std::mutex log_mutex;void background_logger(const std::string& message) {// 模拟耗时的日志写入操作std::this_thread::sleep_for(std::chrono::milliseconds(100));std::lock_guard<std::mutex> lock(log_mutex);std::cout << "[后台日志] " << message << std::endl;
}void handle_client_request(int client_id) {// 创建分离线程处理日志记录std::thread log_thread(background_logger, "客户端" + std::to_string(client_id) + "的请求已处理");log_thread.detach(); // 不关心日志线程何时完成// 主业务逻辑继续执行std::cout << "处理客户端" << client_id << "的请求..." << std::endl;
}int main() {// 模拟处理多个客户端请求for (int i = 1; i <= 3; ++i) {handle_client_request(i);std::this_thread::sleep_for(std::chrono::milliseconds(500));}// 给后台线程一些时间完成std::this_thread::sleep_for(std::chrono::seconds(2));return 0;
}
4.2 常驻进程中的线程使用模式
服务端守护线程示例:
#include <iostream>
#include <thread>
#include <chrono>
#include <atomic>std::atomic<bool> should_exit{false};void signal_handler() {// 等待退出信号std::cout << "信号处理线程启动" << std::endl;while (!should_exit) {std::this_thread::sleep_for(std::chrono::seconds(1));// 模拟检查退出条件static int count = 0;if (++count > 5) {should_exit = true;std::cout << "收到退出信号" << std::endl;}}
}void worker_thread(int id) {std::cout << "工作线程" << id << "启动" << std::endl;while (!should_exit) {// 模拟工作std::this_thread::sleep_for(std::chrono::seconds(2));std::cout << "工作线程" << id << "执行任务..." << std::endl;}std::cout << "工作线程" << id << "退出" << std::endl;
}int main() {// 创建信号处理线程(分离)std::thread signal_thread(signal_handler);signal_thread.detach();// 创建工作线程(不分离,主线程需要管理)std::thread worker1(worker_thread, 1);std::thread worker2(worker_thread, 2);// 主线程等待工作线程结束worker1.join();worker2.join();std::cout << "所有线程正常退出" << std::endl;return 0;
}
5. 现实世界类比理解
餐厅服务类比
- 进程 = 整个餐厅
- 主线程 = 餐厅老板(常驻,管理全局)
- 工作线程 = 服务员(处理具体任务)
- 线程分离 = 经理不关心某个服务员何时下班(系统自动处理)
这个分离的服务员依旧是属于餐厅中的一个员工。只不过老板不关心这个员工的工作情况和结果。如果餐厅倒闭了,这个员工也就没有工作了。
6. 使用注意事项和最佳实践
6.1 分离线程的陷阱
// 错误示例:访问已销毁的局部变量
void problematic_detach() {int local_data = 42;std::thread t([&local_data]() {std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << local_data << std::endl; // 危险!local_data可能已销毁});t.detach();
} // 函数返回,local_data被销毁,但分离线程可能还在运行// 正确做法:值捕获或确保生命周期
void safe_detach() {auto data = std::make_shared<int>(42);std::thread t([data]() { // 值捕获,共享所有权std::this_thread::sleep_for(std::chrono::seconds(1));std::cout << *data << std::endl; // 安全});t.detach();
}
6.2 分离线程的适用场景总结
- 后台任务:日志记录、数据备份等
- 事件监听:网络心跳包、信号处理等
- 一次性任务:不需要结果的计算任务
- 资源清理:定时清理缓存、连接池维护等
POSIX线程分离
POSIX线程库中的线程分离函数的详细说明在:POSIX线程库–详细函数说明2.1-CSDN博客
在线程2中我们讲了线程的退出和线程的等待,但是还没有讲线程的分离,所以这一节我们来看看线程分离的内容。
1. 线程分离的基本概念与POSIX接口
1.1 什么是线程分离
线程分离是POSIX线程库提供的一种线程管理机制,通过pthread_detach()函数实现。分离后的线程在结束时系统会自动回收其资源,无需其他线程调用pthread_join()进行等待。
1.2 POSIX线程分离接口
#include <pthread.h>int pthread_detach(pthread_t thread);
- 返回值:成功返回0,失败返回错误码
- 参数:要分离的线程ID
2. 关键问题深度实验分析
2.1 分离线程后的现象观察实验
实验代码:观察分离线程的行为
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>void* long_running_task(void* arg) {int id = *(int*)arg;for (int i = 0; i < 5; ++i) {printf("线程%d执行第%d步\n", id, i);sleep(1);}printf("线程%d正常结束\n", id);return NULL;
}int main() {pthread_t t1, t2;int id1 = 1, id2 = 2;// 创建两个线程pthread_create(&t1, NULL, long_running_task, &id1);pthread_create(&t2, NULL, long_running_task, &id2);// 分离第一个线程,等待第二个线程if (pthread_detach(t1) != 0) {perror("pthread_detach失败");return -1;}// 等待第二个线程结束if (pthread_join(t2, NULL) != 0) {perror("pthread_join失败");return -1;}printf("主线程退出\n");return 0;
}
编译命令:
gcc -o thread_detach thread_detach.c -lpthread
观察结果分析:
- 线程t1被分离后,主线程不会等待它完成
- 线程t2需要pthread_join,主线程会等待t2完成
- 分离线程t1的输出可能不完整,因为主线程退出时进程终止
2.2 进程退出对分离线程的影响验证
验证代码:进程退出时分离线程的命运
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>void* infinite_worker(void* arg) {int count = 0;while (1) {printf("分离线程运行中...计数: %d\n", ++count);sleep(1);if (count >= 3) {printf("分离线程完成3次循环\n");break;}}printf("分离线程正常退出\n");return NULL;
}int main() {pthread_t t;if (pthread_create(&t, NULL, infinite_worker, NULL) != 0) {perror("线程创建失败");return -1;}// 立即分离线程if (pthread_detach(t) != 0) {perror("线程分离失败");return -1;}printf("主线程快速退出,观察分离线程是否继续\n");// 主线程立即退出,不等待分离线程// 这里给一点时间观察现象sleep(1);return 0;
}
关键发现验证:
- 进程退出时,所有线程(包括分离线程)都会被强制终止
- 分离线程的"分离"只影响资源回收,不影响生命周期依赖
3. 线程分离的底层机制深度解析
3.1 线程状态管理机制
线程状态转换示意图:
创建线程(pthread_create)↓
可结合状态(joinable) ←→ 运行中↓ (pthread_detach)
分离状态(detached) ←→ 运行中↓ (线程结束)
自动资源回收
3.2 资源回收机制对比
非分离线程的资源管理:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* worker(void* arg) {printf("工作线程开始\n");sleep(2);printf("工作线程结束\n");return NULL;
}int main() {pthread_t t;// 创建线程pthread_create(&t, NULL, worker, NULL);// 必须调用join,否则资源泄漏pthread_join(t, NULL);printf("主线程结束\n");return 0;
}
分离线程的自动资源回收:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>void* worker(void* arg) {printf("分离工作线程开始\n");sleep(2);printf("分离工作线程结束\n");return NULL;
}int main() {pthread_t t;pthread_create(&t, NULL, worker, NULL);// 分离线程,自动回收资源pthread_detach(t);// 不需要也不能调用pthread_join// pthread_join(t, NULL); // 错误!// 给分离线程时间完成sleep(3);printf("主线程结束\n");return 0;
}
3.3 资源回收策略注意事项
详细说明在POSIX线程库–详细函数说明2.1-CSDN博客中pthread_detach函数部分–>资源回收的详细说明
| 资源类型 | 系统是否自动回收 | 管理责任 | 示例 |
|---|---|---|---|
| 线程栈空间 | ✅ 是 | 系统 | int local_var; char buffer[100]; |
| 线程描述符 | ✅ 是 | 系统 | 线程ID、状态信息等 |
| 动态分配内存 | ❌ 否 | 程序员 | malloc(), calloc()分配的内存 |
| 文件描述符 | ❌ 否 | 程序员 | open(), fopen()返回的fd |
| 互斥锁等同步对象 | ❌ 否 | 程序员 | pthread_mutex_t, 需要手动destroy |
✅ 系统自动回收的资源:
- 线程栈空间(局部变量)
- 线程描述符和内核数据结构
- 线程状态信息等系统资源
❌ 需要手动管理的资源:
malloc(),calloc()分配的堆内存fopen()打开的文件描述符pthread_mutex_init()创建的互斥锁等同步对象- 其他任何动态分配的资源
关键原则:
- 分离线程 ≠ 自动内存管理
- 动态分配的资源必须显式释放
- 文件描述符等系统资源必须正确关闭
- 在线程结束前确保所有资源都被妥善处理
4. 实际应用场景与POSIX实现
4.1 服务器后台任务处理
POSIX线程实现的日志服务示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <string.h>
#include <time.h>pthread_mutex_t log_mutex = PTHREAD_MUTEX_INITIALIZER;void* background_logger(void* arg) {char* message = (char*)arg;// 模拟耗时的日志写入操作sleep(1);pthread_mutex_lock(&log_mutex);time_t now = time(NULL);printf("[%ld] 后台日志: %s\n", now, message);pthread_mutex_unlock(&log_mutex);return NULL;
}void handle_client_request(int client_id) {char message[100];snprintf(message, sizeof(message), "客户端%d的请求已处理", client_id);pthread_t log_thread;// 创建日志线程if (pthread_create(&log_thread, NULL, background_logger, strdup(message)) != 0) {perror("日志线程创建失败");return;}// 分离日志线程if (pthread_detach(log_thread) != 0) {perror("日志线程分离失败");}// 主业务逻辑继续执行printf("处理客户端%d的请求...\n", client_id);
}int main() {// 模拟处理多个客户端请求for (int i = 1; i <= 3; ++i) {handle_client_request(i);sleep(1);}// 给后台线程一些时间完成sleep(3);pthread_mutex_destroy(&log_mutex);return 0;
}
4.2 常驻进程的线程管理模式
服务端守护进程的POSIX实现:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <signal.h>
#include <stdatomic.h>atomic_int should_exit = 0;void* signal_handler(void* arg) {printf("信号处理线程启动\n");for (int i = 0; i < 5; ++i) {if (should_exit) break;sleep(1);printf("检查系统状态...%d/5\n", i + 1);// 模拟收到退出信号if (i == 4) {should_exit = 1;printf("收到退出信号\n");}}return NULL;
}void* worker_thread(void* arg) {int id = *(int*)arg;printf("工作线程%d启动\n", id);while (!should_exit) {sleep(2);printf("工作线程%d执行任务...\n", id);}printf("工作线程%d退出\n", id);return NULL;
}int main() {pthread_t signal_thread, worker1, worker2;int id1 = 1, id2 = 2;// 创建信号处理线程(分离)if (pthread_create(&signal_thread, NULL, signal_handler, NULL) != 0) {perror("信号线程创建失败");return -1;}pthread_detach(signal_thread);// 创建工作线程(不分离,需要join)pthread_create(&worker1, NULL, worker_thread, &id1);pthread_create(&worker2, NULL, worker_thread, &id2);// 主线程等待工作线程结束pthread_join(worker1, NULL);pthread_join(worker2, NULL);printf("所有线程正常退出\n");return 0;
}
5. 深入理解线程分离的本质
5.1 线程与进程的关系再认识
验证线程共享进程资源的实验:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/types.h>int global_data = 100; // 全局变量,所有线程共享void* detached_worker(void* arg) {printf("分离线程PID: %d, 线程ID: %lu\n", getpid(), (unsigned long)pthread_self());// 修改全局数据global_data = 200;printf("分离线程修改global_data为: %d\n", global_data);sleep(2);printf("分离线程看到的global_data: %d\n", global_data);return NULL;
}int main() {printf("主线程PID: %d, 线程ID: %lu\n", getpid(), (unsigned long)pthread_self());pthread_t t;pthread_create(&t, NULL, detached_worker, NULL);pthread_detach(t);// 主线程也修改全局数据sleep(1);global_data = 300;printf("主线程修改global_data为: %d\n", global_data);// 等待分离线程结束sleep(2);printf("最终global_data: %d\n", global_data);return 0;
}
实验结果证明:
- 分离线程与主线程有相同的PID(属于同一进程)
- 分离线程仍然共享全局数据
- 线程分离不改变资源共享的本质
5.2 分离线程的错误处理模式
健壮的线程分离代码示例:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
#include <stdlib.h>void* worker_with_cleanup(void* arg) {// 线程清理处理程序pthread_cleanup_push([](void* arg) {printf("执行清理操作: %s\n", (char*)arg);}, "释放资源");printf("工作线程开始\n");for (int i = 0; i < 3; i++) {printf("工作进度: %d/3\n", i + 1);sleep(1);// 模拟可能出错的情况if (i == 1) {printf("发生错误,线程提前退出\n");pthread_exit(NULL);}}pthread_cleanup_pop(1); // 执行清理函数printf("工作线程正常结束\n");return NULL;
}int main() {pthread_t t;int ret;// 创建线程ret = pthread_create(&t, NULL, worker_with_cleanup, NULL);if (ret != 0) {fprintf(stderr, "线程创建失败: %s\n", strerror(ret));return -1;}// 分离线程ret = pthread_detach(t);if (ret != 0) {fprintf(stderr, "线程分离失败: %s\n", strerror(ret));// 分离失败时需要joinpthread_join(t, NULL);return -1;}printf("主线程继续执行,不等待工作线程\n");sleep(4); // 确保工作线程有足够时间完成return 0;
}
6. 最佳实践
POSIX线程分离的最佳实践
- 分离时机选择:
// 创建后立即分离(推荐)
pthread_t t;
pthread_create(&t, NULL, worker, NULL);
pthread_detach(t);// 或者在线程内部自我分离
void* worker(void* arg) {pthread_detach(pthread_self());// ... 工作逻辑
}
- 错误处理模式:
int result = pthread_detach(thread_id);
if (result != 0) {if (result == EINVAL) {// 线程不是可结合状态} else if (result == ESRCH) {// 线程ID不存在}// 相应的错误处理
}
- 资源管理注意事项:
- 分离线程中避免使用主线程栈上的变量
- 确保分离线程访问的资源生命周期足够长
- 使用适当的同步机制保护共享资源
提问:
使用c++中的线程类函数来分离线程和使用POSIX线程库中的函数来分离线程有什么不同嘛?尤其是资源回收方面。我比较好奇的就是:我知道使用POSIX线程库回收线程资源是只回收部分资源的(比如线程栈空间,线程描述符合内核数据结构等待),是不会回收线程中申请的堆内存等资源的。那么使用c++中的线程类函数,它的资源回收策略是什么样的呢?也是和POSIX线程库中的资源回收策略一样吗?
回答:
C++11线程与POSIX线程分离机制对比
1. 底层实现架构分析
1.1 C++11线程库的底层依赖
// C++11线程库在Linux上的典型实现架构
// std::thread 底层通常封装pthread
class thread {
private:pthread_t _M_id; // 底层pthread句柄// ... 其他成员
public:// 构造函数、析构函数等
};
1.2 资源回收机制对比架构
C++11 std::thread↓ (封装)
POSIX pthread↓ (系统调用)
Linux内核线程
2. 资源回收策略深度解析
2.1 POSIX线程资源回收机制
POSIX线程资源分类:
#include <pthread.h>
#include <stdlib.h>
#include <stdio.h>void* pthread_worker(void* arg) {// 线程栈上分配的资源(自动回收)int stack_array[100];// 堆上分配的资源(需要手动管理)int* heap_data = malloc(100 * sizeof(int));// 文件资源(需要手动管理)FILE* file = fopen("temp.txt", "w");printf("线程运行中...\n");sleep(2);// 必须手动释放堆资源!free(heap_data);fclose(file);return NULL;
}int main() {pthread_t t;pthread_create(&t, NULL, pthread_worker, NULL);pthread_detach(t); // 只回收线程系统资源// 注意:detach不回收heap_data和file资源!sleep(3);return 0;
}
POSIX线程分离回收的具体资源:
- ✅ 线程内核数据结构
- ✅ 线程栈空间
- ✅ 线程描述符
- ❌ 堆内存分配
- ❌ 文件描述符
- ❌ 动态库资源
2.2 C++11线程资源回收机制
C++11线程资源管理示例:
#include <iostream>
#include <thread>
#include <memory>
#include <fstream>void cpp11_worker() {// 栈上对象(自动析构)std::string stack_string = "栈上字符串";// 智能指针管理的堆资源(自动释放)auto heap_data = std::make_unique<int[]>(100);// RAII文件对象(自动关闭)std::ofstream file("temp.txt");std::cout << "C++11线程运行中..." << std::endl;std::this_thread::sleep_for(std::chrono::seconds(2));// 不需要手动释放!RAII自动处理
}int main() {std::thread t(cpp11_worker);t.detach(); // 分离线程std::this_thread::sleep_for(std::chrono::seconds(3));return 0;
}
3. 核心差异深度分析
3.1 资源管理哲学的根本不同
POSIX线程(C风格)资源管理:
// 手动资源管理模式
void* pthread_example(void* arg) {// 资源获取void* resource1 = acquire_resource_1();void* resource2 = acquire_resource_2();// 使用资源use_resources(resource1, resource2);// 必须手动释放!release_resource_2(resource2);release_resource_1(resource1);return NULL;
}
C++11线程(RAII模式)资源管理:
// RAII自动资源管理
void cpp11_example() {// 资源获取即初始化RAII_Resource resource1 = acquire_resource_1();RAII_Resource resource2 = acquire_resource_2();// 使用资源use_resources(resource1, resource2);// 自动释放!析构函数处理
}
3.2 内存回收策略对比实验
实验代码:验证资源回收差异
#include <iostream>
#include <thread>
#include <memory>
#include <cstdlib>// C++11线程函数
void cpp11_memory_test() {auto smart_ptr = std::make_unique<int>(42);std::cout << "C++11线程: 智能指针管理的内存" << std::endl;// 智能指针离开作用域自动释放
}// 用于pthread的函数
void* pthread_memory_test(void* arg) {int* raw_ptr = new int(42); // 原始指针std::cout << "Pthread: 原始指针分配的内存" << std::endl;// 必须手动删除!delete raw_ptr;return NULL;
}int main() {std::cout << "=== C++11线程测试 ===" << std::endl;{std::thread t1(cpp11_memory_test);t1.detach();}std::cout << "=== Pthread测试 ===" << std::endl;{pthread_t t2;pthread_create(&t2, NULL, pthread_memory_test, NULL);pthread_detach(t2);}std::this_thread::sleep_for(std::chrono::seconds(1));// 使用valgrind等工具检测内存泄漏std::cout << "测试完成,检查内存泄漏..." << std::endl;return 0;
}
4. 异常安全性的关键差异
4.1 C++11线程的异常安全保证
异常安全示例:
#include <iostream>
#include <thread>
#include <memory>
#include <stdexcept>void exception_worker() {auto resource = std::make_unique<int[]>(100);try {// 可能抛出异常的操作if (rand() % 2) {throw std::runtime_error("随机错误!");}std::cout << "操作成功" << std::endl;}catch (const std::exception& e) {std::cout << "捕获异常: " << e.what() << std::endl;// resource仍然会被正确释放!}// 无论是否异常,resource都会被自动释放
}int main() {std::thread t(exception_worker);t.detach();std::this_thread::sleep_for(std::chrono::seconds(1));return 0;
}
4.2 POSIX线程的异常安全问题
POSIX线程异常处理挑战:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>// C语言没有异常机制,需要使用错误码
void* pthread_exception_unsafe(void* arg) {int* data = malloc(100 * sizeof(int));// 模拟错误情况if (rand() % 2) {// 在C++中抛出异常(危险!)// throw std::runtime_error("错误"); // 这会导致未定义行为// 必须使用错误码free(data); // 手动释放return NULL;}// 正常处理free(data); // 必须手动释放return NULL;
}
5. 实际应用中的资源管理策略
5.1 C++11最佳实践:RAII完全体
完整的RAII资源管理示例:
#include <iostream>
#include <thread>
#include <memory>
#include <vector>
#include <fstream>class ThreadSafeLogger {
private:std::ofstream file;std::mutex mutex;public:ThreadSafeLogger(const std::string& filename) : file(filename) {}void log(const std::string& message) {std::lock_guard<std::mutex> lock(mutex);if (file.is_open()) {file << message << std::endl;}}// 析构函数自动关闭文件~ThreadSafeLogger() {if (file.is_open()) {file.close();}}
};void raii_worker(std::shared_ptr<ThreadSafeLogger> logger) {// 所有资源都是RAII管理的auto data = std::make_shared<std::vector<int>>(100, 42);logger->log("线程开始工作");// 模拟工作std::this_thread::sleep_for(std::chrono::milliseconds(500));logger->log("线程工作完成");// 所有资源自动释放:// - data的智能指针// - logger的共享指针// - 函数栈上的所有对象
}int main() {auto logger = std::make_shared<ThreadSafeLogger>("log.txt");std::thread t(raii_worker, logger);t.detach();std::this_thread::sleep_for(std::chrono::seconds(1));// logger在main结束时自动释放return 0;
}
5.2 POSIX资源管理的最佳实践
POSIX线程的完整资源管理:
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>typedef struct {int* data;size_t size;FILE* log_file;
} thread_resources_t;void cleanup_resources(void* arg) {thread_resources_t* res = (thread_resources_t*)arg;printf("清理线程资源...\n");if (res->data) {free(res->data);res->data = NULL;}if (res->log_file) {fclose(res->log_file);res->log_file = NULL;}free(res);
}void* pthread_worker(void* arg) {thread_resources_t* resources = malloc(sizeof(thread_resources_t));resources->data = NULL;resources->log_file = NULL;// 注册清理函数pthread_cleanup_push(cleanup_resources, resources);// 分配资源resources->data = malloc(100 * sizeof(int));resources->log_file = fopen("pthread_log.txt", "w");if (!resources->data || !resources->log_file) {pthread_exit(NULL);}// 使用资源fprintf(resources->log_file, "Pthread工作线程\n");sleep(1);// 手动清理或通过cleanup_pop清理pthread_cleanup_pop(1); // 执行清理函数return NULL;
}int main() {pthread_t t;pthread_create(&t, NULL, pthread_worker, NULL);pthread_detach(t);sleep(2);return 0;
}
6. 核心结论总结
6.1 资源回收策略对比表
| 资源类型 | POSIX pthread_detach | C++11 thread::detach | 说明 |
|---|---|---|---|
| 线程内核资源 | ✅ 自动回收 | ✅ 自动回收 | 通过底层pthread实现 |
| 线程栈空间 | ✅ 自动回收 | ✅ 自动回收 | 操作系统管理 |
| 堆内存(原始指针) | ❌ 手动管理 | ❌ 手动管理 | 程序员责任 |
| 堆内存(智能指针) | 不适用 | ✅ RAII自动管理 | C++特有机制 |
| 文件描述符 | ❌ 手动管理 | ✅ RAII自动管理 | C++文件流类 |
| 互斥锁等同步原语 | ❌ 手动管理 | ✅ RAII自动管理 | std::lock_guard等 |
6.2 根本差异的本质
C++11线程分离的优势:
- RAII原则:资源获取即初始化,析构时自动释放
- 异常安全:异常发生时资源仍然被正确释放
- 类型安全:编译时检查资源管理错误
- 代码简洁:减少手动资源管理代码
底层事实:
- C++11的
std::thread::detach()在Linux上底层调用pthread_detach() - 线程系统资源的回收机制完全相同
- 差异在于应用程序级资源的管理策略
- C++11通过RAII模式提供了更高级别的资源安全保证
6.3 实际建议
- 新项目:优先使用C++11线程,享受RAII带来的资源安全
- 现有pthread项目:可以逐步引入RAII包装器
- 性能关键:理解底层机制,避免不必要的抽象开销
- 资源敏感:C++11提供更安全的资源生命周期管理
