【Linux】线程封装

目录

个人主页:矢望

个人专栏:C++LinuxC语言数据结构Coze-AIMySQL

一、线程封装

线程类内的成员变量需要线程名,线程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_nppthread_getname_npLinux 提供的非标准(np = non-portable)扩展函数,用于设置和获取线程的名称。这个名称是直接设置在线程库中的,名称长度有上限,通常为16字节

c 复制代码
int pthread_setname_np(pthread_t thread, const char *name);
  • thread :要设置名称的线程 IDpthread_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账号,我们一同成长!
(~ ̄▽ ̄)~

相关推荐
艾莉丝努力练剑13 小时前
【QT】常用控件(三)Qt布局管理器(网格/表单/间隔器)
java·linux·运维·服务器·开发语言·网络·qt
小猫咪0113 小时前
Linux 日志系统入门:/var/log 和 journalctl 怎么排查问题?
linux
muddjsv13 小时前
Linux主流发行版:版本介绍、核心异同与精准场景选型
linux
艾莉丝努力练剑13 小时前
【Linux网络】Linux 网络编程:传输层TCP(一)
linux·运维·服务器·网络·tcp/ip·计算机网络
lys0796200013 小时前
kali linux 配置固定IP
linux·网络·tcp/ip
Cx330❀13 小时前
【Linux网络】从以太网碰撞到 Socket 套接字与网络字节序的深度解析
xml·linux·运维·服务器·开发语言·网络·c++
ahdkk13 小时前
Linux或者 mac 系统多版本 jdk切换配置
java·linux·macos
t5y2221 小时前
【Linux】定时任务调度
linux·服务器
HY小海1 天前
【Linux】进程概念
linux·运维·服务器