【Linux】线程封装

目录

个人主页:矢望

个人专栏: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;
  }

Main.cc

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;
};

Main.cc:

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账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
道满科技5 小时前
Function Calling 参数总出错?只修参数就行
linux·windows·python
Android系统攻城狮6 小时前
Linux Gstreamer深度解析之gst_audio_ring_buffer_prepare_read调用流程与实战(七十四)
linux·运维·服务器·gstreamer音视频·音视频进阶
我科绝伦(Huanhuan Zhou)6 小时前
Linux 通用压力测试脚本合集:磁盘 + 网络
linux·网络·压力测试
Soneswy9 小时前
Linux系统篇--重定向与文件操作命令
linux
有毒的教程9 小时前
Linux底层存储路由配
linux·运维·网络
Linux系统运维工程师_郭主任10 小时前
运维人还在被拒offer?现在能考的,只有 RHCE 10.0。
linux
自养Agent10 小时前
MCP Server Boundary Audit Report
linux·服务器·人工智能·windows·大模型·llm
Coisinier11 小时前
docker命令管理镜像及容器
linux·运维·nginx·docker·容器·操作系统
A心有千千结11 小时前
通过 SSH 反向代理让服务器借用本机网络访问 GitHub 的最佳实践
linux·服务器·网络·git·ssh·github
书生执笔画浮沉12 小时前
Linux 内核模块为什么不能自己解决兼容性问题?
linux·c·kernel·update·灵感