目录

个人主页:矢望
个人专栏:C++、Linux、C语言、数据结构、Coze-AI、MySQL
一、线程封装
线程类内的成员变量需要线程名,线程id,线程状态等,成员函数需要线程创建、线程终止、线程等待、线程分离等,所以线程类的基本框架是这样的:
cpp
#include <iostream>
#include <string>
#include <pthread.h>
namespace ThreadModule
{
static int gnumber = 1; // 线程名称后面的编号
enum class STATUS // 线程状态的枚举值
{
THREAD_NEW,
THREAD_RUNNING,
THREAD_STOP
};
class Thread
{
public:
Thread()
: _tid(-1)
, _status(STATUS::THREAD_NEW)
{
_name = "New-Thread-" + std::to_string(gnumber++);
}
~Thread()
{}
void Start() // 线程创建
{
}
void Join() // 线程等待
{
}
void Die() // 线程终止
{
}
void Detach() // 线程分离
{
}
private:
std::string _name; // 线程名称
STATUS _status; // 线程状态
pthread_t _tid; // 线程 id
};
}
上面的代码框架中,线程的状态使用了枚举值表示。
线程创建
cpp
class Thread
{
private:
void ToRunning()
{
this->_status = STATUS::THREAD_RUNNING;
}
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
}
public:
Thread()
: _tid(-1)
, _status(STATUS::THREAD_NEW)
{
_name = "New-Thread-" + std::to_string(gnumber++);
}
//...
bool Start() // 线程创建
{
int n = pthread_create(&_tid, nullptr, Routine, this);
if(n != 0)
return false;
ToRunning(); // 修改进程状态为运行状态
return true;
}
//...
private:
std::string _name; // 线程名称
STATUS _status; // 线程状态
pthread_t _tid; // 线程 id
};
如上代码,在进行线程创建的时候,线程的执行函数Routine是必须是static的,因为这个函数的参数只有一个void*,但是在类内创建函数时,默认就会带有this指针,所以在类内创建的函数默认会多一个参数,而使用static修饰时,就是说这个函数是全体类成员的,不是某一个成员的,此时函数就没有this指针参数了。
回想我们之前使用pthread_create创建出线程之后,接下来,就是让线程执行我们安排的任务,所以线程类中还缺少一个成员去存储下达的任务。
将来用户就会在上层,给线程派发任务,所以我们需要有一个回调函数成员去存储任务。
因此就会变成这样:
cpp
namespace ThreadModule
{
static int gnumber = 1; // 线程名称后面的编号
using callback_t = std::function<void ()>; // 定义回调函数类型:返回值为 void,参数列表为空
class Thread
{
private:
void ToRunning()
{
this->_status = STATUS::THREAD_RUNNING;
}
void ToStop()
{
this->_status = STATUS::THREAD_STOP;
}
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
self->_cb(); // 回调执行用户给的任务
self->ToStop(); // 修改进程状态为终止状态
return nullptr;
}
public:
Thread(callback_t cb) // 用户下达的任务
: _tid(-1)
, _status(STATUS::THREAD_NEW)
, _cb(cb)
{
_name = "New-Thread-" + std::to_string(gnumber++);
}
bool Start() // 线程创建
{
int n = pthread_create(&_tid, nullptr, Routine, this);
if(n != 0)
return false;
ToRunning(); // 修改进程状态为运行状态
return true;
}
private:
std::string _name; // 线程名称
STATUS _status; // 线程状态
pthread_t _tid; // 线程 id
callback_t _cb; // 回调函数成员
};
}
用户层代码:
cpp
void loop()
{
while(true)
{
std::cout << "new thread running..." << std::endl;
sleep(1);
}
}
int main()
{
ThreadModule::Thread t(loop); // 给线程类下达 loop 任务
t.Start();
sleep(100);
return 0;
}
用户层向线程类传递了loop任务,等线程执行Start时就会回调执行loop任务。
编译运行:

如上图,新线程正常执行loop任务。
上图中的右边,我们在查询的时候,新线程的名字是和主线程相同的,这样不太好看,这里有两个函数可以设置、获取名字,分别是pthread_setname_np、pthread_getname_np。
pthread_setname_np 和 pthread_getname_np 是 Linux 提供的非标准(np = non-portable)扩展函数,用于设置和获取线程的名称。这个名称是直接设置在线程库中的,名称长度有上限,通常为16字节。
c
int pthread_setname_np(pthread_t thread, const char *name);
thread:要设置名称的线程ID(pthread_t类型)。name:要设置的名称字符串。
c
int pthread_getname_np(pthread_t thread, char *name, size_t len);
thread:要获取名称的线程ID。name:输出缓冲区,用于存放获取到的名称。len:缓冲区大小(通常传16,因为名称最长为16字节含'\0')。
Thread.hpp
cpp
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
pthread_setname_np(self->_tid, self->_name.c_str()); // 设置名称到线程库中
self->_cb(); // 回调执行用户给的任务
self->ToStop(); // 修改进程状态为终止状态
return nullptr;
}
cpp
void loop()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
while(true)
{
std::cout << name << " running..." << std::endl;
sleep(1);
}
}
因为执行loop函数的执行流将来就是新线程,所以使用pthread_self就可以获取新线程的线程id。
再次编译运行:

线程分离、线程终止、线程等待
线程分离:
cpp
class Thread
{
public:
Thread(callback_t cb) // 用户下达的任务
: _tid(-1)
, _status(STATUS::THREAD_NEW)
, _cb(cb)
, _joinable(true)
{
_name = "New-Thread-" + std::to_string(gnumber++);
}
//...
void Detach() // 线程分离
{
if(_status == STATUS::THREAD_RUNNING && _joinable)
{
pthread_detach(_tid); // 分离
_joinable = false;
}
else
{
std::cerr << "detach" << _name << "failed" << std::endl;
}
}
private:
std::string _name; // 线程名称
STATUS _status; // 线程状态
pthread_t _tid; // 线程 id
callback_t _cb; // 回调函数成员
bool _joinable; // 是否可以被等待
};
如上,又多了一个成员变量_joinable表示线程是否可以被等待,默认是可以被等待的,但分离之后就不可以等待了。
线程终止:
cpp
void Die() // 线程终止
{
if(_status == STATUS::THREAD_RUNNING)
{
pthread_cancel(_tid);
_status = STATUS::THREAD_STOP;
}
}
线程等待:
cpp
void Join() // 线程等待
{
if(_joinable)
{
int n = pthread_join(_tid, &_result);
if(n != 0)
{
std::cerr << "join error, " << n << std::endl;
return;
}
(void)_result;
_status = STATUS::THREAD_STOP;
}
else
{
std::cerr << "error, thread join status is false" << std::endl;
return;
}
}
如上代码,又增加了一个成员变量void *_result存储线程的返回值。
相关调试函数:
cpp
std::string Status2String(STATUS s)
{
switch(s)
{
case STATUS::THREAD_NEW:
return "THREAD_NEW";
case STATUS::THREAD_RUNNING:
return "THREAD_RUNNING";
case STATUS::THREAD_STOP:
return "THREAD_STOP";
default:
return "UNKNOWN";
}
}
std::string IsJoined(bool joinable)
{
return joinable ? "true" : "false";
}
void PrintInfo()
{
std::cout << "thread name : " << _name << std::endl;
std::cout << "thread _tid : " << _tid << std::endl;
std::cout << "thread _status : " << Status2String(_status) << std::endl;
std::cout << "thread _joinable : " << IsJoined(_joinable) << std::endl;
}
测试代码1:
cpp
oid loop()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
int cnt = 5;
while(cnt--)
{
std::cout << name << " running..., cnt: " << cnt << std::endl;
sleep(1);
}
}
int main()
{
ThreadModule::Thread t(loop); // 给线程类下达 loop 任务
t.Start();
t.Join();
t.PrintInfo();
return 0;
}
如上代码,创建出新线程之后,让它跑5s正常退出之后,等待它。
编译运行:

如上,线程等待成功,并且线程的状态也符合我们的预期。
测试代码2:
cpp
void loop()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
int cnt = 10;
while(cnt--)
{
std::cout << name << " running..., cnt: " << cnt << std::endl;
sleep(1);
}
}
int main()
{
ThreadModule::Thread t(loop); // 给线程类下达 loop 任务
t.Start();
t.Detach(); // 分离线程
sleep(5);
t.Join();
t.PrintInfo();
return 0;
}
如上代码,我们创建新线程之后就分离它,然后等待5s之后再进行线程等待,我们应该线程等待失败。
编译运行:

如上,线程分离之后,再次进行线程等待时失败。这也符合我们的预期:分离和等待是互斥的操作,二者只能选其一。
测试代码3:
cpp
void loop()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
int cnt = 10;
while(cnt--)
{
std::cout << name << " running..., cnt: " << cnt << std::endl;
sleep(1);
}
}
int main()
{
ThreadModule::Thread t(loop); // 给线程类下达 loop 任务
t.Start();
t.Detach(); // 分离线程
sleep(5);
t.Die();
t.PrintInfo();
return 0;
}
如上,我们等待5s之后,终止进程。
编译运行:

如上,线程终止成功。
多线程
要创建多个线程就需要对它们先描述,再组织。上面的线程类就是对线程的描述,所以我们之后只需要对线程通过vector、queue等进行组织就好了。
这里以vector为例:
cpp
void loop()
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
int cnt = 1;
while(cnt--)
{
std::cout << name << " running..., cnt: " << cnt << std::endl;
sleep(1);
}
}
int main()
{
std::vector<ThreadModule::Thread> threads;
int num = 5;
for(int i = 0; i < 5; i++) // 创建 num 个线程
{
threads.emplace_back(loop);
}
for(auto& t : threads) // 启动 num 个线程
{
t.Start();
}
for(auto& t : threads) // 等待 num 个线程
{
t.Join();
t.PrintInfo();
sleep(1);
}
return 0;
}
编译运行:

如上图,这样就可以创建多线程了!
传参问题
如果将来用户的loop函数需要传递参数的话,可以将线程类改成模板类,相关回调函数也改成模板的,这样就可以将参数传递进去了。
Thread.hpp:
cpp
namespace ThreadModule
{
static int gnumber = 1; // 线程名称后面的编号
template<typename T> // 模板回调
using callback_t = std::function<void (T &)>; // 定义回调函数类型:返回值为 void,参数为 T 的左值引用
// ...
template<typename T>
class Thread
{
private:
void ToRunning()
{
this->_status = STATUS::THREAD_RUNNING;
}
void ToStop()
{
this->_status = STATUS::THREAD_STOP;
}
static void *Routine(void *args)
{
Thread *self = static_cast<Thread *>(args);
pthread_setname_np(self->_tid, self->_name.c_str()); // 设置名称到线程库中
self->_cb(self->_data); // 回调执行用户给的任务
self->ToStop(); // 修改进程状态为终止状态
return nullptr;
}
public:
// 在构造函数中接收参数
Thread(callback_t<T> cb, T data) // 用户下达的任务
: _tid(-1)
, _status(STATUS::THREAD_NEW)
, _cb(cb)
, _joinable(true)
, _result(nullptr)
, _data(data)
{
_name = "New-Thread-" + std::to_string(gnumber++);
}
~Thread()
{}
bool Start() // 线程启动(创建)
{
int n = pthread_create(&_tid, nullptr, Routine, this);
if(n != 0)
return false;
ToRunning(); // 修改进程状态为运行状态
return true;
}
// ...
private:
std::string _name; // 线程名称
STATUS _status; // 线程状态
pthread_t _tid; // 线程 id
callback_t<T> _cb; // 回调函数成员
bool _joinable; // 是否可以被等待
void *_result; // 线程退出信息
T _data; // 存储用户层传递的参数
};
}
线程类这样修改就可以支持传递参数了。
例如用户需要传递int参数:
cpp
void loop(int x)
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
std::cout << name << " 获得参数, x: " << x << std::endl;
}
int main()
{
std::vector<ThreadModule::Thread<int>> threads;
int num = 5;
for(int i = 0; i < 5; i++) // 创建 num 个线程
{
threads.emplace_back(loop, 10); // 传递 loop 函数 及 参数
}
for(auto& t : threads) // 启动 num 个线程
{
t.Start();
}
for(auto& t : threads) // 等待 num 个线程
{
t.Join();
t.PrintInfo();
sleep(1);
}
return 0;
}
编译运行:

如上,所有的线程都拿到了参数。
此时就会有人产生疑问,如果要传递的参数是可变参数呢?那怎么办?
模板类T,能传递的又不只有整型等单一类型,可以传递类、结构体类型呀。
多参数传递:
cpp
class ThreadData
{
public:
ThreadData(int a, int b, int c)
: _a(a)
, _b(b)
, _c(c)
{}
public:
int _a;
int _b;
int _c;
// 其它参数
};
void loop(ThreadData &td)
{
char name[64];
pthread_getname_np(pthread_self(), name, sizeof name); // 获取线程名
printf("%s 获得参数: _a: %d, _b: %d, _c: %d\n", name, td._a, td._b, td._c);
}
int main()
{
ThreadData td(10, 20, 33);
std::vector<ThreadModule::Thread<ThreadData>> threads;
int num = 5;
for(int i = 0; i < 5; i++) // 创建 num 个线程
{
threads.emplace_back(loop, td); // 传递 loop 函数 及 参数
}
for(auto& t : threads) // 启动 num 个线程
{
t.Start();
}
for(auto& t : threads) // 等待 num 个线程
{
t.Join();
t.PrintInfo();
sleep(1);
}
return 0;
}
编译运行:

如上,照样可以传递多参数,将来如果你的参数是变化的,你想要什么参数类型就向ThreadData中加就可以了。
当然我们将来不需要对它进行传参,传参会导致模块与模块之间的耦合度增加。
任务模块
我们将来进行使用的时候,线程模块和任务模块是解耦的,线程模块会执行任务模块的代码,然后执行结果会自动保存在任务模块中。任务对象本身携带输入参数和输出结果,无需额外传参或全局变量。
线程模块只负责"何时执行"和"如何执行",任务模块负责"执行什么"和"结果放哪",两者通过接口解耦,任务对象既是输入也是输出的容器。
我们的任务模块就放上一个加法类。
cpp
class Task
{
public:
Task(int x, int y)
:_x(x)
,_y(y)
{}
void Excute()
{
_result = _x + _y;
}
std::string Result()
{
return std::to_string(_x) + " + " + std::to_string(_y) + " = " + std::to_string(_result);
}
~Task(){}
private:
int _x;
int _y;
int _result;
};
cpp
int main()
{
Task task(10, 30);
// 传递线程要执行的参数为空,返回值为 void 的匿名函数
ThreadModule::Thread t([&task]()->void{
task.Excute(); // 要执行的任务
});
t.Start(); // 线程启动
t.Join(); // 等待线程
std::cout <<task.Result() << std::endl; // 查看执行结果
return 0;
}
如上,我们直接设定任务,然后让给线程的回调函数设置要执行的匿名函数即可,线程执行时会回调执行传递的匿名函数,然后任务类就会保存任务执行的结果。线程执行任务结束之后,等待线程即可,最后我们将任务类中保存的结果打印出来。
注意,我们将Thread.hpp的版本改成之前的了,不再是模板类了。另外如果先 Result() 再 Join(),可能读到未计算完成的中间结果,上面的代码可以保证线程执行任务完毕之后再查看结果。
任务对象携带输入和输出,线程通过 Lambda 调用任务方法,执行结果自动存储在任务对象中,等待线程结束后安全读取。
编译运行:

如上,线程顺利完成任务。
总结:
以上就是本期博客分享的全部内容啦!如果觉得文章还不错的话可以三连支持一下,你的支持就是我前进最大的动力!
技术的探索永无止境! 道阻且长,行则将至!后续我会给大家带来更多优质博客内容,欢迎关注我的CSDN账号,我们一同成长!
(~ ̄▽ ̄)~