C++多线程学习02 线程的入口参数

一,对象作为入口参数的生命周期

有时候主线程中要给子线程通过线程的构造函数传些数据,因为多个线程可能访问同样的数据,因此需要将每次创建线程的时候需要将这些数据做个拷贝存放于一个列表中:
其构造函数的一部分:

    template <class _Fn, class... _Args, enable_if_t<!is_same_v<_Remove_cvref_t<_Fn>, thread>, int> = 0>
    explicit thread(_Fn&& _Fx, _Args&&... _Ax) {
    
    
        using _Tuple                 = tuple<decay_t<_Fn>, decay_t<_Args>...>;
        auto _Decay_copied           = _STD make_unique<_Tuple>(_STD forward<_Fn>(_Fx), _STD forward<_Args>(_Ax)...);
        constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof...(_Args)>{
    
    });

使用了函数模板,因此不用限定数据的类型,使用_Invoker_proc指向该列表。

来做个实例,将函数名,整型,浮点型,字符串型,自定义对象做为参数来构造线程:

class Para
{
    
    
public:
    Para() {
    
     cout << "Create Para" << endl; }
    Para(const Para& p) {
    
     cout << "Copy Para" << endl; }//参数做复制时对象进行了拷贝构造函数
    ~Para() {
    
     cout << "Drop Para" << endl; }
    string name;
};

void ThreadMain(int p1, float p2, string str, Para p4)
{
    
    
    this_thread::sleep_for(100ms);//sleep100毫秒后已经运行过了AAA,f1等参数已经被释放,但由于参数做了复制,因此没问题
    cout << "ThreadMain " << p1 << " " << p2 << " " << str <<" "<<p4.name<< endl;
}

int main(int argc, char* argv[])
{
    
    
    thread th;
    {
    
    
    float f1 = 12.1f;
    Para p;//第一次Create
    p.name = "test Para class";
    //所有的参数做复制,第一次Copy(),拷贝在一个链表中,然后再把这个链表的值传给进程的句柄,回调函数的入口参数第二次Copy
    th =  thread(ThreadMain, 101, f1, "test string para",p);
    }//AAA
    th.join();
    return 0;
}

在这里插入图片描述
Para p;时打印Create Para。
在线程的构造函数中 constexpr auto _Invoker_proc = _Get_invoke<_Tuple>(make_index_sequence<1 + sizeof…(_Args)>{});打印第一个Copy Para。
将_Invoker_proc指向的数据传给线程包装的函数时打印二次Copy Para。

二,指针和引用作为线程函数的参数

在01的学习中传递的是时候将数据复制一份,不会造成问题,如果传的是指针或引用的话会有2个问题:
01多个线程就会访问同一块空间,02主线程可能会提取释放这个空间
子线程

子线程:

void ThreadMain(int p1, float p2, string str, Para p4)
{
    
    
    this_thread::sleep_for(100ms);
    cout << "ThreadMain " << p1 << " " << p2 << " " << str <<" "<<p4.name<< endl;
}

void ThreadMainPtr(Para* p)
{
    
    
    this_thread::sleep_for(100ms);
    cout << "ThreadMainPtr name = " << p->name << endl;
}

void ThreadMainRef(Para& p)
{
    
    
    this_thread::sleep_for(100ms);
    cout << "ThreadMainPtr name = " << p.name << endl;
}

为了确保子线程不会先结束,sleep_for(100ms);

主线程:

int main(int argc, char* argv[])
{
    
    
    {
    
    
       //01
        Para p;
        p.name = "test ref";
        thread th(ThreadMainRef, ref(p));
        th.join();
    }
    getchar();
      //02
    {
    
    
        Para p;
        p.name = "test ThreadMainPtr name";
        thread th(ThreadMainPtr, &p); //错误 ,线程访问的p空间会提前释放
        th.detach();
    }
    getchar();
    //03
    {
    
    
        Para p;
        p.name = "test ThreadMainPtr name";
        thread th(ThreadMainPtr, &p);
        th.join();
        
    }
    getchar();
    //04
    thread th;
    {
    
    
    float f1 = 12.1f;
    Para p;
    p.name = "test Para class";
    //所有的参数做复制
    th =  thread(ThreadMain, 101, f1, "test string para",p);
    }
    th.join();
    return 0;
}

注意thread th(ThreadMainRef, ref§);
ref()方法的返回值是reference_wrapper类型,这个类的源码大概的意思就是维持一个指针,并且重载操作符。
ref()能用包装类型reference_wrapper来代替原本会被识别的值类型,而reference_wrapper能隐式转换为被引用的值的引用类型。
std::ref 用于包装按引用传递的值。
thread的方法使用函数模板,传递引用的时候,必须外层用ref来进行引用传递,否则会编译出错。

在这里插入图片描述
可以看到02中无法访问para中的成员,这是因为虽然将子线程与主线程detach了,但只是将其句柄的周期交给了子线程,para的生命周期随着 }结束而结束

01与03中使用join(),虽然访问的成员没有经过复制,但用join()阻塞了主线程,使其访问问para后主线程才将释放,因此不会出现问题

04中即使将para的创建放在 {}中,其生命周期随着{}的结束而结束,但para在线程的构造函数中进行了复制,因此不会出现问题,正常访问。

三、成员函数作为线程入口

实际应用中经常将对象作为一个线程存储,可以将对象的所有成员做为线程的参数,该线程的入口函数就是对象的成员函数。因此线程生命周期与该对象一致,更容易管理

成员函数与普通函数的区别在于成员函数的第一个隐含的指针形参是this指针,通过该指针去访问类对象中的成员。

线程的构造函数只使用类中的函数指针是不够的,还需要对象的this指针

 th_ = std::thread(&XThread::Main, this);

在线程定义时不需要传入参数,而是对对象的成员进行赋值即可

thread th(&MyThread::Main, &myth);

实践下:

class MyThread
{
    
    
public:
    //入口线程函数
    void Main()
    {
    
    
        cout << "MyThread Main " << name << ":" << age;
    }
    string name;
    int age = 100;
};
int main(int argc, char* argv[])
{
    
    
    MyThread myth;
    myth.name = "Test name 001";
    myth.age = 20;
    thread th(&MyThread::Main, &myth);
    th.join();

    return 0;
}

在这里插入图片描述
老铁没毛病!
为了在实际中更方便使用线程,我们往往定义一个通过对象来生成一个线程的基类,然后在使用特定类的时候继承这个基类

基类:

class XThread
{
    
    
public:
    virtual void Start()
    {
    
    
        is_exit_ = false;
        th_ = std::thread(&XThread::Main, this);
    }
    virtual void Stop()
    {
    
    
        is_exit_ = true;
        Wait();
    }
    virtual void Wait()
    {
    
    
        if (th_.joinable())//线程ID号还在不在
            th_.join();
    }
    bool is_exit() {
    
     return is_exit_; }
private:
    virtual void Main() = 0;
    std::thread th_;
    bool is_exit_ = false;
};

if (th_.joinable())//线程ID号(PID)还在不在,在的话判断为真, 然后调用th_.join();将主线程的时间片分给子线程

将main定义为纯虚函数,在通过派生类来完成其定义:

class TestXThread :public XThread
{
    
    
public:
    void Main() override//不用在编译阶段才发现拼写错
    {
    
    
        cout << "TestXThread Main begin" << endl;
        while (!is_exit())
        {
    
    
            this_thread::sleep_for(100ms);
            cout << "." << flush;
        }
        cout << "TestXThread Main end" << endl;
    }
    string name;
};

使用override不用在编译阶段才发现拼写错

主线程调用:

int main(int argc, char* argv[])
{
    
    
    TestXThread testth;
    testth.name = "TestXThread name ";
    testth.Start();
    this_thread::sleep_for(3s);
    testth.Stop();
    testth.Wait();
    getchar();
}

01先新建一个对象,并为其成员赋值,此时对象和线程还无关
02使用testth.Start();将该对象封装成线程,线程入口参数为派生类的main,主线程休息3秒,于是main打了3秒的点
03 3秒结束,testth.Stop()通过is exit来通知子线程进行退出
04 通过testth.Wait();来阻塞主线程等待子线程的退出

在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_42567607/article/details/125461623