【Linux】多线程创建及封装

本篇主要介绍多线程,以及线程的封装,线程控制的相关介绍在【Linux】线程控制

1.创建和等待多线程

我们可以一次创建十个线程,创建的时候直接用for循环创建,参数就传这些线程的名字,然后把这些线程的ID保存在vector里,ID的类型pthread_t其实就是long int类型。

cpp 复制代码
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <string>

const int num = 10;

void* ThreadFunc(void* args)
{
    std::string name = static_cast<const char*>(args);
    while(true)
    {
        std::cout << name << "运行中..." << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::vector<long long> tids; 

    for(int i = 0; i < num; i++) //创建10个线程
    {
        char name[64];
        snprintf(name, sizeof(name), "新线程%d", i); //格式化名字
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, ThreadFunc, name);
        if(n == 0)
            tids.push_back(tid); //创建成功就把新线程的ID存着
        else
            continue;
        sleep(1);
    }
    
    return 0;
}

然后用监控脚本看一下线程。

cpp 复制代码
 while :; do ps -aL ; sleep 1; done

在等待的时候,通过我们保存在vector里的线程ID一个一个等待。

cpp 复制代码
#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <string>

const int num = 10;

void *ThreadFunc(void *args)
{
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << name << "运行中..." << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::vector<long long> tids;

    for (int i = 0; i < num; i++) // 创建10个线程
    {
        char name[64];
        snprintf(name, sizeof(name), "新线程%d", i); // 格式化名字
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, ThreadFunc, name);
        if (n == 0)
            tids.push_back(tid); // 创建成功就把新线程的ID存着
        else
            continue;
        sleep(1);
    }

    for (int i = 0; i < num; i++) //一个一个等待
    {
        int n = pthread_join(tids[i], nullptr);
        if (n == 0)
            std::cout << "等待成功" << std::endl;
        else
            std::cout << "等待失败" << std::endl;
    }

    return 0;
}

但是我们如果在创建线程时for循环里不加sleep,而在ThreadFunc函数最开始加上sleep,会出现如下结果,只有新线程9。

cpp 复制代码
const int num = 10;

void *ThreadFunc(void *args)
{
    sleep(1);
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << name << "运行中..." << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::vector<long long> tids;

    for (int i = 0; i < num; i++) // 创建10个线程
    {
        char name[64];
        snprintf(name, sizeof(name), "新线程%d", i); // 格式化名字
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, ThreadFunc, name);
        if (n == 0)
            tids.push_back(tid); // 创建成功就把新线程的ID存着
        else
            continue;
        //sleep(1);
    }

    return 0;
}

因为name是for循环里的一个临时数组,创建线程的时候传递过去的name是这个数组的起始地址,每一次for循环,编译器都会在同一个栈帧的固定偏移量位置上开辟name的空间,就导致每次for循环不停的开辟、释放、开辟、释放...但是每次开辟到的都是同一个位置,所以给pthread_create传过去的name起始地址就是同样的。

ThreadFunc函数拿到这个值之后sleep一秒才将ThreadFunc里的name改动。

所以就发生了ThreadFunc里的name还没来得及改动,创建线程的for循环就进入下一个循环了,里面的name的内容就被修改了,意思就是指针没变,但是指针指向的内容已经改了

所以这里我们要做如下改动,在堆区给name开辟空间,这样每个线程得到的name就不是一样的了。

cpp 复制代码
#include "Thread.hpp"

const int num = 10;

void *ThreadFunc(void *args)
{
    sleep(1);
    std::string name = static_cast<const char *>(args);
    int cnt = 5;
    while (cnt--)
    {
        std::cout << name << "运行中..." << std::endl;
        sleep(1);
    }
    return nullptr;
}

int main()
{
    std::vector<long long> tids;

    for (int i = 0; i < num; i++) // 创建10个线程
    {
        //char name[64]; //错误写法
        char* name = new char[64]; //正确写法,堆区开辟空间
        snprintf(name, 64, "新线程%d", i); // 格式化名字
        pthread_t tid;
        int n = pthread_create(&tid, nullptr, ThreadFunc, name);
        if (n == 0)
            tids.push_back(tid); // 创建成功就把新线程的ID存着
        else
            continue;
    }

    for (int i = 0; i < num; i++) //一个一个等待
    {
        int n = pthread_join(tids[i], nullptr);
        if (n == 0)
            std::cout << "等待成功" << std::endl;
        else
            std::cout << "等待失败" << std::endl;
    }

    return 0;
}

2.线程封装

先做一下准备工作。

cpp 复制代码
//Thread.hpp文件
#ifndef _THREAD_H_
#define _THREAD_H_

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <string>

namespace MyThread 
{
    class Thread
    {
    public:
    private:
    };
}

#endif
cpp 复制代码
//Main.cc文件
#include "Thread.hpp"

int main()
{
    return 0;
}
bash 复制代码
//Makefile
thread:Main.cc
	g++ -o $@ $^ -lpthread

.PHONY:clean
clean:
	rm -f thread

我们需要设计一下线程的名字,用一个变量代表被创建的线程编号,类成员变量如下。

cpp 复制代码
namespace MyThread
{
    static int num = 1; 
    class Thread
    {
    public:
        Thread()
            : _tid(0),
              _isdetach(false), /*默认不分离*/
              _isrunning(false)
        {
            _name = "new thread-" + std::to_string(num++);
        }
        
    private:
        pthread_t _tid; //线程ID
        std::string _name; //线程的名字
        bool _isdetach; //线程是否设置分离状态
        bool _isrunning; //线程是否启动
    };
}

2.1 线程的启动和分离

设计一个Start接口,启动线程。

cpp 复制代码
bool Start()
{
    int n = pthread_create(&_tid, nullptr, ThreadFunc, nullptr/*暂时先不传参数*/);
    if(n != 0) //创建失败
    {
        std::cerr << "create tread error: " << strerror(n) << std:: endl;
        return false;
    }
    //创建成功
        
}

这里还要考虑线程分离的情况,线程的分离状态可以在线程启动之前就设置,也可以是线程已经启动了,再设置成分离状态。

cpp 复制代码
private:
    void SetRun()
    {
        _isrunning = true;
    }

public:
    void Detach()
    {
        if (_isdetach) // 如果已经分离了,直接返回
            return;
        if (_isrunning)           // 如果线程此时正在运行
            pthread_detach(_tid); // 分离线程
        // 如果此时还没运行,就只设置标志位,Start函数内会调用Detach
        SetDetach();
    }
    void SetDetach()
    {
        std::cout << "线程被分离" << std::endl;
        _isdetach = true;
    }
    bool Start()
    {
        int n = pthread_create(&_tid, nullptr, ThreadFunc /*暂时未实现*/, nullptr /*暂时先不传参数*/);
        if (n != 0) // 创建失败
        {
            std::cerr << "create tread error: " << strerror(n) << std::endl;
            return false;
        }
        // 线程启动前设置分离
        if (_isdetach)
            Detach();
        SetRun(); // 设置状态为run
    }

Detach这样实现就可以保证不管是启动前分离还是启动之后再分离都可以,启动前调用Detach函数,两个if都不满足,就只会设置_isdetach 标志位为false,启动后调用Detach,线程分离并且修改标志位。

2.2 线程停止和等待

取消一个线程就是要确保他是在运行的,取消进程之后,标志位也要更新。

cpp 复制代码
    bool Stop()
    {
        if (_isrunning)
        {
            int n = pthread_cancel(_tid);
            if (n != 0)
            {
                std::cerr << "cancel tread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                _isrunning = false;
                std::cout << _name << "stop success" << std::endl;
            }
        }
        return true;
    }

等待进程不管是不是在运行都可以等待,但是如果被分离了就不用等待了,线程等待的结果我们也可以获取一下。

cpp 复制代码
    bool Join()
    {
        if (_isdetach)
        {
            std::cout << "线程已经被分离,join失败" << std::endl;
            return false;
        }
        int n = pthread_join(_tid, &_ret);
        if (n != 0)
        {
            std::cerr << "join tread error: " << strerror(n) << std::endl;
            return false;
        }
        else
        {
            std::cout << "join success" << std::endl;
        }
        return true;
    }
private:
    pthread_t _tid;    // 线程ID
    std::string _name; // 线程的名字
    bool _isdetach;    // 线程是否设置分离状态
    bool _isrunning;   // 线程是否启动
    void *_ret;        // 线程等待的结果

2.3 新线程入口函数

我们新线程的入口函数不能在类内实现,因为含函数要求参数为void*,但是在类内部的函数第一个参数是隐藏的this指针!!所以这个ThreadFunc函数其实是传了两个参数。

解决方法有很多,这里我选择将ThreadFunc函数设为static,这样就没有this指针了。

cpp 复制代码
static void* ThreadFunc(void* args)
{

}

但是我们并不是为了让线程只执行类内的ThreadFunc,我们创建线程是为了给线程指派任务的,所以任务必须是类外提供的方法。

cpp 复制代码
#include <functional>

class Thread
{
    using func_t = std::function<void()>; //返回值为void,参数为空
    //...
}

意思就是定义一个函数类型func_t,让线程未来执行我所指定的方法,所以这里类成员变量还要添加一个。

cpp 复制代码
class Thread
{
    using func_t = std::function<void()>; // 返回值为void,参数为空

public:
    Thread(func_t func)
        : _tid(0),
          _isdetach(false), /*默认不分离*/
          _isrunning(false),
          _ret(nullptr),
          _func(func)
    {
        _name = "new thread-" + std::to_string(num++);
    }

    //...

private:
    pthread_t _tid;    // 线程ID
    std::string _name; // 线程的名字
    bool _isdetach;    // 线程是否设置分离状态
    bool _isrunning;   // 线程是否启动
    void *_ret;        // 线程等待的结果
    func_t _func;      // 线程执行的方法
}

所以现在我们创建的线程不是为了执行对应的ThreadFunc函数的,而是让这个线程执行我给他指派的任务。此时我们的ThreadFunc函数里就要对_func进行回调。

cpp 复制代码
    static void *ThreadFunc(void *args)
    {
        _func(); //回调处理
        return nullptr; // 返回值不关心
    }

    bool Start()
    {
        if (_isrunning)
            return false; // 线程已经启动了就不能重复启动
        int n = pthread_create(&_tid, nullptr, ThreadFunc, nullptr /*暂时先不传参数*/);
        if (n != 0) // 创建失败
        {
            std::cerr << "create tread error: " << strerror(n) << std::endl;
            return false;
        }
        // 线程启动前设置分离
        if (_isdetach)
            Detach();
        SetRun(); // 设置状态为run
        return true;
    }

但是!这样写是无法调用回调的,因为ThreadFunc函数此时是static的,没有this指针,也就无法访问当前类成员属性。所以这里ThreadFunc函数传参就要传this指针。

cpp 复制代码
   static void *ThreadFunc(void *args) 
    {
        Thread *self = static_cast<Thread *>(args);
        self->_func(); //回调处理
        return nullptr; // 返回值不关心
    }
    bool Start()
    {
        if (_isrunning)
            return false; // 线程已经启动了就不能重复启动
        int n = pthread_create(&_tid, nullptr, ThreadFunc, this); 
        //...
    }

这样ThreadFunc函数就可以在类内访问回调方法了。

并且我们前面的分离和运行的标志位逻辑可以放在ThreadFunc函数,这样代码的可读性更好。

cpp 复制代码
    static void *ThreadFunc(void *args)
    {
        Thread *self = static_cast<Thread *>(args);
        self->SetRun();      // 设置状态为run
        if (self->_isdetach) // 设置分离
            self->Detach();
        self->_func();  // 回调处理
        return nullptr; // 返回值不关心
    }
    bool Start()
    {
        if (_isrunning)
            return false; // 线程已经启动了就不能重复启动
        int n = pthread_create(&_tid, nullptr, ThreadFunc, this);
        if (n != 0) // 创建失败
        {
            std::cerr << "create tread error: " << strerror(n) << std::endl;
            return false;
        }
        else
        {
            std::cout << _name << "create success" << std::endl;
        }
        return true;
    }

2.4 测试

cpp 复制代码
#include "Thread.hpp"
using namespace MyThread;

int main()
{
    Thread t();
    return 0;
}

t后面的括号里就要传要执行的方法,这里可以用Lambda表达式。

cpp 复制代码
int main()
{
    Thread t([]()
             {
        while(true)
        {
            std::cout << "新线程运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空
        
    return 0;
}

此时就由这个Lambda表达式来初始化_func,然后线程对象就被创建出来了。

cpp 复制代码
int main()
{
    Thread t([]()
             {
        while(true) 
        {
            std::cout << "新线程运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空
    t.Start();
    sleep(3);
    t.Stop();
    sleep(3);
    t.Join();
    sleep(3);
    return 0;
}

我们还可以设置Detach,先测试启动前分离。

cpp 复制代码
int main()
{
    Thread t([]()
             {
        while(true) 
        {
            std::cout << "新线程运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空
    t.Detach();    // 设置分离状态
    t.Start();
    sleep(3);
    t.Stop();
    sleep(3);
    t.Join();
    sleep(3);
    return 0;
}

再测试一个启动后再分离线程。

cpp 复制代码
int main()
{
    Thread t([]()
             {
        while(true) 
        {
            std::cout << "新线程运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空

    t.Start();
    t.Detach(); // 设置分离状态
    sleep(3);
    t.Stop();
    sleep(3);
    t.Join();
    sleep(3);
    return 0;
}

2.5 设置和获取线程名称

  • pthread_setname_nppthread_getname_np是两个⽤于设置和获取线程名称的⾮标准函数(_np 表⽰ "non-portable",即⾮可移植的)。它们通常在 Linux 和 其他⼀些类 Unix 系统中可⽤,⽤于调试和多线程程序的管理。

pthread_setname_np 就是传一个线程ID,给这个线程设置个名字,pthread_getname_np就是可以获取指定线程设置的名字。

cpp 复制代码
static void *ThreadFunc(void *args)
{
    Thread *self = static_cast<Thread *>(args);
    self->SetRun();     
    if (self->_isdetach) 
        self->Detach();
    pthread_setname_np(self->_tid, self->_name.c_str()); //设置名字
    self->_func(); 
    return nullptr; 
}
cpp 复制代码
#include "Thread.hpp"
using namespace MyThread;

int main()
{
    Thread t([]()
             {
        while(true) 
        {
            char buffer[64];
            pthread_getname_np(pthread_self(), buffer, sizeof(buffer)); //获取名字
            std::cout << buffer << "运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空

    t.Start();
    t.Detach(); // 设置分离状态
    sleep(3);
    t.Stop();
    sleep(3);
    t.Join();
    sleep(3);
    return 0;
}
  • 线程名称⻓度限制: 在 Linux 上,线程名称的最⼤⻓度为 16 个字符(包括结尾的 \0 ),如果名称超过这个⻓度,会被截断。
  • 权限: 通常,只有线程⾃⾝可以设置⾃⼰的名称。尝试设置其他线程的名称可能会导致错误。

其实就是把pthread_setname_np设置的名字写入到了线程的局部存储,get的时候就从局部存储里取,局部存储只有线程自己能访问。

下面是Tread.hpp的代码。

cpp 复制代码
#ifndef _THREAD_H_
#define _THREAD_H_

#include <iostream>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <vector>
#include <cstring> //strerror要包含的头文件
#include <string>
#include <functional>

namespace MyThread
{
    static int num = 1;
    class Thread
    {
        using func_t = std::function<void()>; // 返回值为void,参数为空

    private:
        void SetRun()
        {
            _isrunning = true;
        }

        void SetDetach()
        {
            std::cout << "线程被分离" << std::endl;
            _isdetach = true;
        }

        static void *ThreadFunc(void *args)
        {
            Thread *self = static_cast<Thread *>(args);
            self->SetRun();      // 设置状态为run
            if (self->_isdetach) // 设置分离
                self->Detach();
            pthread_setname_np(self->_tid, self->_name.c_str()); //设置名字
            self->_func();  // 回调处理
            return nullptr; // 返回值不关心
        }

    public:
        Thread(func_t func)
            : _tid(0),
              _isdetach(false), /*默认不分离*/
              _isrunning(false),
              _ret(nullptr),
              _func(func)
        {
            _name = "new thread-" + std::to_string(num++);
        }

        void Detach()
        {
            if (_isdetach) // 如果已经分离了,直接返回
                return;
            if (_isrunning)           // 如果线程此时正在运行
                pthread_detach(_tid); // 分离线程
            // 如果此时还没运行,就只设置标志位,Start函数内会调用Detach
            SetDetach();
        }

        bool Start()
        {
            if (_isrunning)
                return false; // 线程已经启动了就不能重复启动
            int n = pthread_create(&_tid, nullptr, ThreadFunc, this);
            if (n != 0) // 创建失败
            {
                std::cerr << "create tread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << _name << " create success" << std::endl;
            }
            return true;
        }

        bool Stop()
        {
            if (_isrunning)
            {
                int n = pthread_cancel(_tid);
                if (n != 0)
                {
                    std::cerr << "cancel tread error: " << strerror(n) << std::endl;
                    return false;
                }
                else
                {
                    _isrunning = false;
                    std::cout << _name << " stop success" << std::endl;
                }
            }
            return true;
        }

        bool Join()
        {
            if (_isdetach)
            {
                std::cout << "线程已经被分离,join失败" << std::endl;
                return false;
            }
            int n = pthread_join(_tid, &_ret);
            if (n != 0)
            {
                std::cerr << "join tread error: " << strerror(n) << std::endl;
                return false;
            }
            else
            {
                std::cout << "join success" << std::endl;
            }
            return true;
        }

        ~Thread()
        {
        }

    private:
        pthread_t _tid;    // 线程ID
        std::string _name; // 线程的名字
        bool _isdetach;    // 线程是否设置分离状态
        bool _isrunning;   // 线程是否启动
        void *_ret;        // 线程等待的结果
        func_t _func;      // 线程执行的方法
    };
}

#endif

3.带模板参数的封装

cpp 复制代码
namespace MyThread
{
    static int num = 1;

    template <typename T> // 模板
    class Thread
    {
        using func_t = std::function<void(T)>; // 返回值为void,参数为类型为T

    private:

        //...

        static void *ThreadFunc(void *args)
        {
            Thread<T> *self = static_cast<Thread<T> *>(args); //全部改为模板
            self->SetRun();      
            if (self->_isdetach) 
                self->Detach();
            self->_func(self->_data);  // 回调处理,参数为T
            return nullptr; 
        }

    public:
        Thread(func_t func, T data) 
            : _tid(0),
              _isdetach(false), 
              _isrunning(false),
              _ret(nullptr),
              _func(func),
              _data(data)  //这个参数就是回调函数的参数
        {
            _name = "new thread-" + std::to_string(num++);
        }

        //...

    private:
        pthread_t _tid;    // 线程ID
        std::string _name; // 线程的名字
        bool _isdetach;    // 线程是否设置分离状态
        bool _isrunning;   // 线程是否启动
        void *_ret;        // 线程等待的结果
        func_t _func;      // 线程执行的方法
        T _data;           // 传给执行方法的参数
    };
}

#endif

除了上面给出来的接口要做改变,其他接口都不变。

cpp 复制代码
#include "Thread.hpp"
using namespace MyThread;

void Conter(int num)
{
    while (num--)
    {
        std::cout << "新进程运行中..., " << num << std::endl;
        sleep(1);
    }
}

int main()
{
    int cnt = 5;
    Thread<int> t(Conter, cnt); // cnt是Conter的参数
    t.Start();
    t.Join();
    return 0;
}

这里的cnt是Conter的参数,就类似于pthread_create函数的第四个参数是第三个参数的参数。

现在我们可以传int类型的参数 ,当然也可以传自定义类型,这里就不演示了。

4.多线程

多线程很简单,代码都不用改,就是在Main.cc里直接for循环创建。

cpp 复制代码
#include "Thread.hpp"
#include <vector>
using namespace MyThread;

int main()
{
    std::vector<Thread> threads;
    for (int i = 0; i < 5; i++)
    {
        Thread t([](){
        while(true) 
        {
            char buffer[64];
            pthread_getname_np(pthread_self(), buffer, sizeof(buffer)); //获取名字
            std::cout << buffer << "运行中..." << std::endl;
            sleep(1);
        } }); // 返回值为void,参数为空
        threads.emplace_back(t);
    }

    for (auto &t : threads)
    {
        t.Start();
    }
    
    sleep(3); //3秒后全部停止并回收
    for (auto &t : threads)
    {
        t.Stop();
    }
    for (auto &t : threads)
    {
        t.Join();
    }

    return 0;
}

本篇分享就到这里了,我们下篇见~

相关推荐
Nᴏsᴛᴀʟɢɪᴀ念4 小时前
多线程奇幻漂流:从单核到多核质变(一)
java·开发语言·jvm·多线程
艾菜籽7 小时前
JVM中的垃圾回收机制
java·jvm
꒰ঌ 安卓开发໒꒱16 小时前
Java面试-并发面试(一)
java·jvm·面试
讓丄帝愛伱2 天前
Arthas & JVM 性能排查
linux·jvm
铅笔侠_小龙虾3 天前
JVM深入研究--JHSDB (jvm 分析工具)
java·开发语言·jvm
码码哈哈0.04 天前
从“类加载的五个阶段”逐步分析 JVM“ 在每个阶段的内存变化”
jvm
寻星探路4 天前
Java EE初阶启程记04---线程的状态
java·开发语言·jvm·java-ee
wshzrf5 天前
【Java系列课程·Java学前须知】第3课 JDK,JVM,JRE的区别和优缺
java·开发语言·jvm
铅笔侠_小龙虾5 天前
JVM 深入研究 -- 详解class 文件
java·开发语言·jvm