Linux线程——对线程库简单的面向对象封装
文章目录
- 对线程库简单的面向对象封装
- 常规版本——基础使用
- 线程库面向对象的思考过程
- 线程类代码前置准备
- 基础版本源码
- 进阶版本——带有模板参数的使用
- 用户传参的需求
- 进阶源码
- 进阶使用
对线程库简单的面向对象封装
其实这个工作c++11已经做好了。但是,为了能够更清晰地理解线程控制接口的相关使用,同时理解c++线程库的基本使用,在这里,我们尝试着对Linux的原生线程库pthread进行简单的封装!
注意:
这里我们只是为了更深地理解线程库地使用。以及锻炼面向对象封装的过程。
但是,在线程部分,我们还是又很多的内容没有讲到的,如线程同步、互斥、共享数据保护…
本篇文章只是基于当前已有认知上,简单地对pthread进行封装!故有些地方是有问题的!
常规版本——基础使用
首先,不管那么多,先简单地对代码封装进行实现,并且使用一下。然后将在基础版本的代码之上,进行一些扩展使用!
线程库面向对象的思考过程
我们翻开c++文档,我们可以发现,线程的很多操作都被封装成了面向对象的接口!即抽象化出要调用的方法,底层的实现是被藏起来的!我们的目标就是如此。
首先,我们需要对线程面向对象的使用有一个基本的认知:
1.我们希望的是,能够通过创建一个Thread对象就能成功创建一个线程(完成一些线程基本属性创建)
2.通过接口Start来启动线程,即让线程开始执行代码
3.通过接口Detach来让线程分离
4.通过接口Stop终止线程
5.通过接口Join来回收线程
需要说明的是:
上述的所有接口,都是站在主线程的角度来看待的。所以也就是说,上述的接口都是主线程操作创建的线程!
线程类代码前置准备
前置准备其实也就是搭建线程类基本框架:
如线程类的成员变量,构造,有哪些接口,引入相关头文件:
#pragma once#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<functional>namespace myThread{// 直接用包装器包装一个调用函数 这个函数就是未来创建线程执行的任务!using func_t = std::function<void()>;static uint32_t thread_id = 1;class Thread{public: Thread(func_t func):_tid(0),_func(func),_is_detach(false),_is_running(false),_result(nullptr){char* name = new char[64]{0};snprintf(name, 64, "thread_%d", thread_id++);_name = name;delete[] name;}// 析构函数什么也不用写~Thread(){}// 启动线程bool Start(){}// 分离线程void Detach(){}// 终止线程void Stop(){}// 回收线程void Join(){}private:pthread_t _tid; // 线程的idfunc_t _func; // 线程未来执行的任务bool _is_detach; // 判断线程是否需要分离bool _is_running; // 判断线程是否处于运行状态void* _result; // 线程结束后的返回值std::string _name; // 线程名字};
}
上述就是基础框架。我们这里需要说明:
c++线程库在使用的时候,创建对象的时候就需要我们手动的把创建线程的执行任务传入。所以,这里我的实现中,用包装器function包装一个void (void)类型的函数作为线程执行的任务函数!
(不过,线程最终执行函数的时候,是(void*) (void*),这个不怕,到时候进行一层封装即可,即把真正要调用的函数放在类内,让类内函数回调外界传入的函数)。
然后还通过一个计数器来给线程命名。不过这里我们这样写是会出现问题的,需要我们通过sleep来控制每个线程创建的时间间隔,否则可能会影响每个线程拿到的名字!
我们还需要设置线程的相关状态:如线程是否分离、线程是否运行。
因为很多时候,需要根据这两个状态来判断当前操作是否合法:
1.如线程已经运行起来了,那就不能重复Start!
2.线程运行起来的设置分离和未运行的设置分离是有区别的。前者需要手动的在系统层面设置分离,后者只需要设置状态即可。
3.Join内部需要判断是否有分离。如果有就不进行回收。
…
在成员变量中,还添加了一个返回值void* res,这个其实是用来接收返回值的。不过基础版本下,我们并不关心返回值,所以这里统一设为nullptr:
基础版本源码
这里就直接上源代码和使用代码了,都有注释,就不解释了:
Thread.hpp
#pragma once#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<functional>namespace myThread{// 直接用包装器包装一个调用函数 这个函数就是未来创建线程执行的任务!using func_t = std::function<void()>;static uint32_t thread_id = 1;class Thread{private:// 通过Linux下特有的函数,将线程名字设置到 “线程局部存储”void SetThreadName(){int setname_id = pthread_setname_np(_tid, _name.c_str());if(setname_id == 0) {std::cout << "线程名字" << _name << "被设置到系统中" << std::endl;}else {std::cerr << "线程名字" << _name << "被设置到系统失败" << std::endl;}}void SetDetach(){std::cout << "线程" << _name << "分离" << std::endl;_is_detach = true;}void SetRunning(){_is_running = true;}void DisRunning() {_is_running = false;}// 不加static是有问题的,不能传给pthread_create的函数指针 -> 因为类内函数默认有this指针(两个参数)static void* Routine(void* args){Thread* self = static_cast<Thread*>(args);// 调用函数// 这里就先不管如何通过函数把返回值带出来,等到实现一份带有模板参数的就可以了self->_func();// 这个函数到这里结束了,马上设置运行状态self->DisRunning();return nullptr;}// 但是,加了static,没有this指针,没办法用类内的变量和函数! 所以,调用这个函数的时候传。// 所以,这就是为什么pthread_create最后一个参数传的是this指针public: Thread(func_t func):_tid(0),_func(func),_is_detach(false),_is_running(false),_result(nullptr){char* name = new char[64]{0};snprintf(name, 64, "thread_%d", thread_id++);_name = name;delete[] name;}// 析构函数什么也不用写~Thread(){}// 启动线程bool Start(){// 如果线程已经启动,那就直接返回 -> 不能让线程重复启动if(_is_running){std::cout << "线程" << _name << "已经被创建!" << std::endl;return false;}// 这个Routine函数是从外部传来的void(),让类内部的void*(void*)函数进行回调else{int createThread_id = pthread_create(&_tid, nullptr, Routine, this);if(createThread_id == 0) {std::cout << "线程" << _name << "创建成功" << std::endl;SetThreadName(); // 把名字设置到系统内SetRunning(); // 将该进程设置为运行状态 // 为了防止出现一种情况:即在线程Start之前就Detach// 如果这里不这么做,会导致系统层面并没有真正的Detach该线程if(_is_detach) SetDetach();return true;return true;}else{std::cerr << "线程" << _name << "创建失败" << std::endl;return false;}}}// 分离线程void Detach(){// 已经设置了分离了,那就不操作了if(_is_detach) return;if(_is_running){// 如果当前线程已经跑起来了,那么就需要手动的调用接口来设置分离pthread_detach(_tid);}SetDetach();}// 终止线程void Stop(){// 只有线程处于运行状态才要终止if(_is_running){int cancelThread_id = pthread_cancel(_tid);if(cancelThread_id == 0) {std::cout << "线程" << _name << "终止" << std::endl;DisRunning();}else {std::cout << "线程" << _name << "终止失败" << std::endl;}}}// 回收线程void Join(){if(_is_detach){std::cerr << "线程" << _name << ", tid[" << _tid << "]" << "被分离, 无法回收" << std::endl;}else{// 线程的回收不需要判断线程是否还在运行int joinThread_id = pthread_join(_tid, nullptr); // 先不关心返回值的问题if(joinThread_id == 0){std::cerr << "线程" << _name << "回收成功" << std::endl;}}}private:pthread_t _tid; // 线程的idfunc_t _func; // 线程未来执行的任务bool _is_detach; // 判断线程是否需要分离bool _is_running; // 判断线程是否处于运行状态void* _result; // 线程结束后的返回值std::string _name; // 线程名字};}
Main.cpp
#include "Thread.hpp"using namespace myThread;
#include <vector>int main(){auto l1 = []{//int cnt = 10;while(1){std::cout << "i am a thread" << std::endl;sleep(1);}};std::vector<Thread> _threads;for(int i = 0; i < 5; ++i){Thread thread(l1);_threads.push_back(thread);}for(int i = 0; i < 5; ++i){_threads[i].Start();}int cnt = 10;while(cnt--){std::cout << "main thread..." << std::endl;sleep(1);}for(int i = 0; i < 5; ++i){_threads[i].Stop();_threads[i].Join();}return 0;
}
注: 使用代码均采用创建多个现成的办法来进行测试,且没有对共享资源做数据保护(如显示器文件),所以打印出来的结果可能是混乱的!
进阶版本——带有模板参数的使用
用户传参的需求
这里会有一种需求:
就是希望在用户层中,给Thread类传递若干个参数。这些参数就是给线程执行的函数来调用。
在c++线程库下,采用的是可变模板参数实现!即用户可以传递若干个参数。但是,使用模板参数是比较麻烦的,需要考虑到很多c++的新特性。
所以,在这里为了简单模拟实现这个功能,我们采用模板参数!如果想要传递多个参数,就把这些参数都放在一个类下。
进阶源码
#pragma once#include<iostream>
#include<unistd.h>
#include<pthread.h>
#include<functional>namespace myThread{// 直接用包装器包装一个调用函数 这个函数就是未来创建线程执行的任务!static uint32_t thread_id = 1;template<class T>class Thread{private:using func_t = std::function<void(T)>;// 通过Linux下特有的函数,将线程名字设置到 “线程局部存储”void SetThreadName(){int setname_id = pthread_setname_np(_tid, _name.c_str());if(setname_id == 0) {std::cout << "线程名字" << _name << "被设置到系统中" << std::endl;}else {std::cerr << "线程名字" << _name << "被设置到系统失败" << std::endl;}}void SetDetach(){std::cout << "线程" << _name << "分离" << std::endl;_is_detach = true;}void SetRunning(){_is_running = true;}void DisRunning() {_is_running = false;}// 不加static是有问题的,不能传给pthread_create的函数指针 -> 因为类内函数默认有this指针(两个参数)static void* Routine(void* args){Thread* self = static_cast<Thread*>(args);// 调用函数// 这里就先不管如何通过函数把返回值带出来,等到实现一份带有模板参数的就可以了self->_func(self->_data);// 这个函数到这里结束了,马上设置运行状态self->DisRunning();return nullptr;}// 但是,加了static,没有this指针,没办法用类内的变量和函数! 所以,调用这个函数的时候传。// 所以,这就是为什么pthread_create最后一个参数传的是this指针public: Thread(func_t func, T data):_tid(0),_func(func),_is_detach(false),_is_running(false),_result(nullptr),_data(data){char* name = new char[64]{0};snprintf(name, 64, "thread_%d", thread_id++);_name = name;delete[] name;}// 析构函数什么也不用写~Thread(){}// 启动线程bool Start(){// 如果线程已经启动,那就直接返回 -> 不能让线程重复启动if(_is_running){std::cout << "线程" << _name << "已经被创建!" << std::endl;return false;}// 这个Routine函数是从外部传来的void(),让类内部的void*(void*)函数进行回调else{int createThread_id = pthread_create(&_tid, nullptr, Routine, this);if(createThread_id == 0) {std::cout << "线程" << _name << "创建成功" << std::endl;SetThreadName(); // 把名字设置到系统内SetRunning(); // 将该进程设置为运行状态 // 为了防止出现一种情况:即在线程Start之前就Detach// 如果这里不这么做,会导致系统层面并没有真正的Detach该线程if(_is_detach) SetDetach();return true;}else{std::cerr << "线程" << _name << "创建失败" << std::endl;return false;}}}// 分离线程void Detach(){// 已经设置了分离了,那就不操作了if(_is_detach) return;if(_is_running){// 如果当前线程已经跑起来了,那么就需要手动的调用接口来设置分离pthread_detach(_tid);}SetDetach();}// 终止线程void Stop(){// 只有线程处于运行状态才要终止if(_is_running){int cancelThread_id = pthread_cancel(_tid);if(cancelThread_id == 0) {std::cout << "线程" << _name << "终止" << std::endl;DisRunning();}else {std::cout << "线程" << _name << "终止失败" << std::endl;}}}// 回收线程void Join(){if(_is_detach){std::cerr << "线程" << _name << ", tid[" << _tid << "]" << "被分离, 无法回收" << std::endl;}else{// 线程的回收不需要判断线程是否还在运行int joinThread_id = pthread_join(_tid, nullptr); // 先不关心返回值的问题if(joinThread_id == 0){std::cerr << "线程" << _name << "回收成功" << std::endl;}}}// 获取到当前线程的idpthread_t GetTid() const{return _tid;}// 获取当前线程名字std::string GetName(){return _name;}private:pthread_t _tid; // 线程的idfunc_t _func; // 线程未来执行的任务bool _is_detach; // 判断线程是否需要分离bool _is_running; // 判断线程是否处于运行状态void* _result; // 线程结束后的返回值std::string _name; // 线程名字T _data; // 线程执行函数的时候用到的参数};}
其实就是在Thread类内,用到了外界传参的地方,增加一个模板参数!
进阶使用
Main.cpp
#include "Thread.hpp"using namespace myThread;
#include <vector>// 放在main函数中不好操作class ThreadTask{
public:ThreadTask(int taski, int cnt = 10):_taski(taski),_cnt(cnt),_exec_res(0),_tid(0),_name(""){}~ThreadTask(){}void Execute(){_exec_res = _cnt + 5;}int _taski; int _cnt;int _exec_res;pthread_t _tid;std::string _name;
};std::vector<Thread<ThreadTask>> _threads;
std::vector<ThreadTask> _threadtask;void Routine(ThreadTask data){// 沉睡一下,保证线程启动的时候,数据已经设置到thread_task数组了sleep(1);data._name = _threadtask[data._taski]._name;data._tid = _threadtask[data._taski]._tid;int cnt = data._cnt;while(cnt--){std::cout << data._name << "thread, tid is " << data._tid << std::endl;sleep(1); }std::cout << data._name << " : " << std::endl;std::cout << "执行Execute任务" << std::endl; data.Execute();std::cout << "执行任务结果" << data._exec_res << std::endl;
}int main(){//std::vector<Thread<ThreadTask>> _threads;//std::vector<ThreadTask> _threadtask;// 创建一批线程for(int i = 0; i < 10; ++i){ThreadTask thread_task(i, 5);_threadtask.push_back(thread_task);Thread<ThreadTask> thread(Routine, thread_task);_threads.push_back(thread);// 错误写法!!! 这里push_back调用了移动构造(Thread内没写拷贝 / 移动构造)// 移动构造本质是掠夺资源 -> 一旦访问被移动的有资源的类型(function...) -> 线程崩溃!// 所以,如果写下面两句代码// 第一个线程跑起来一会儿,就访问到了 thread_task._tid = thread.GetTid();// 访问到了被移动的资源 -> 直接崩/* thread_task._name = thread.GetName();thread_task._tid = thread.GetTid(); */// 这样写不用怕报错 -> 因为资源被略到数组上了/* thread_task._name = _threads[i].GetName();thread_task._tid = _threads[i].GetTid(); */// 但是,这样子即使线程跑起来了,也没办法让Routine执行的时候拿到这里thread_task修改的数据// 因为,Thread<ThreadTask> thread(Routine, thread_task);创建的时候// 底层保存的T _data 其实是thread_task的拷贝// 我们在主线程中对thread_task修改,其他线程中保存的都是拷贝,所以是没办法给到那里的!}// 启动线程for(int i = 0; i < _threads.size(); ++i){_threads[i].Start();_threadtask[i]._name = _threads[i].GetName();_threadtask[i]._tid = _threads[i].GetTid();} /* for(int i = 0; i < _threads.size(); ++i){_threads[i].Detach();} */for(int i = 0; i < _threads.size(); ++i){_threads[i].Join();}return 0;
}
使用结果:
这里能够简单验证使用结果即可。
还是因为没有对共享资源保护的原因,会导致出现一些奇怪现象!