对线程库简单的面向对象封装
其实这个工作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; // 线程的id
func_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; // 线程的id
func_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;
}
}
}
// 获取到当前线程的id
pthread_t GetTid() const{
return _tid;
}
// 获取当前线程名字
std::string GetName(){
return _name;
}
private:
pthread_t _tid; // 线程的id
func_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;
}
使用结果:
这里能够简单验证使用结果即可。
还是因为没有对共享资源保护的原因,会导致出现一些奇怪现象!