17.2 C++并发与多线程-线程启动、结束与创建线程写法

17.1 C++并发与多线程-基础概念与实现
17.2 C++并发与多线程-线程启动、结束与创建线程写法
17.3 C++并发与多线程-线程传参详解、detach坑与成员函数作为线程函数
17.4 C++并发与多线程-创建多个线程、数据共享问题分析与案例代码
17.5 C++并发与多线程-互斥量的概念、用法、死锁演示与解决详解
17.6 C++并发与多线程-unique_lock详解
17.7 C++并发与多线程-单例设计模式共享数据分析、解决与call_once
17.8 C++并发与多线程-condition_variable、wait、notify_one与notify_all
17.9 C++并发与多线程-async、future、packaged_task与promise
17.10 C++并发与多线程-future其他成员函数、shared_future与atomic
17.11 C++并发与多线程-Windows临界区与其他各种mutex互斥量
17.12 C++并发与多线程-补充知识、线程池浅谈、数量谈与总结

2.线程启动、结束与创建线程写法

  2.1 范例演示线程运行的开始和结束

void myprint()
{
    cout << "我的线程开始执行了" << endl;
    //...
    //Sleep(1000); //休息1000毫秒(1秒)
    cout << "我的线程执行完毕了" << endl;	
    return;
}
{
    std::thread mytobj(myprint);   //这就是创建线程的代码 ,显然这是个线程对象,然后给的参数是个函数名,代表这个线程是从myprint这个函数(初始函数)开始运行 
    mytobj.join();					//join会卡在这里,等待myprint线程执行完毕,程序流程才会继续往下走		
    cout << "main主函数执行结束!" << endl; //这行由主线程执行,主线程从main返回,则整个进程执行完毕
}

在这里插入图片描述

    观察这个结果,仔细查看输出的信息顺序:先执行所创建线程对应的函数,然后执行main主函数中的cout语句输出“main主函数执行结束!”,最后整个程序结束。

(1)thread

    thread是C++标准库里面的类,这个类就是用来创建线程的。可以看到,用这个类生成一个对象,名字为mytobj,里面是一个可调用对象(此处的可调用对象是函数myprint)作为thread构造函数的实参来构造这个thread对象。

(2)join

    从字面翻译来看,join的意思是“加入/汇合”。换句话说,就是“阻塞”的意思——主线程等待子线程执行完毕,执行流程最终汇合到一起(子线程执行完毕,执行流程回归主线程并执行完main主函数)。
    所以,join成员函数的功能是:用来等待myprint函数(线程的入口函数,也就是代表自己创建的这个线程)运行完成。一旦执行了join这行代码,主线程就阻塞到这一行,等待mytobj对象所代表的线程执行完毕,也就是等待myprint函数执行完毕。
    如果把mytobj.join();代码行注释掉,那么程序运行起来会报异常,而且程序的输出结果也是乱序的,例如可能是如下的输出
在这里插入图片描述
    通过这个乱序结果不难发现,还没等子线程执行完毕,主线程先执行完毕了。这问题就来了,请想一想:子线程正在执行中(没执行完),主线程执行完了,这会导致整个进程退出了。这样的程序代码是不稳定、不合格的,编写这样代码的程序员也是不称职的。
    所以,一个书写良好的程序,应该是主线程等待子线程执行完毕后,自己才能最终退出。这就是上面这条join语句的必要性。现在把join代码行的注释取消,再次看看结果。如下的结果顺序才是正确的:

在这里插入图片描述

(3)detach

    detach是“分离”的意思。所谓分离,就是主线程不和子线程汇合了,主线程执行主线程的,子线程执行子线程的,主线程不必等子线程运行结束,可以先执行结束,这并不影响子线程的执行。
    范例改造一下

void myprint()
{
    cout << "我的线程执行完毕了1" << endl;
    cout << "我的线程执行完毕了2" << endl;
    cout << "我的线程执行完毕了3" << endl;
    cout << "我的线程执行完毕了4" << endl;
    cout << "我的线程执行完毕了5" << endl;
    cout << "我的线程执行完毕了6" << endl;
    cout << "我的线程执行完毕了7" << endl;
    cout << "我的线程执行完毕了8" << endl;
    cout << "我的线程执行完毕了9" << endl;
    cout << "我的线程执行完毕了10" << endl;
    cout << "我的线程执行完毕了11" << endl;
    cout << "我的线程执行完毕了12" << endl;
    return;
}
{
    thread mytobj(myprint); //创建一个线程,也可以称为创建一个子线程
    mytobj.detach();
    cout << "main主函数执行结束!" << endl;
}

在这里插入图片描述
    这里多次执行,看一看结果,其实每次结果可能都有差别,不一样:
    可以看到,有时候看不到线程myprint输出的任何结果,有时候能看到myprint输出了多行结果,然后,因为主线程执行完毕,可执行程序(进程)退出执行,所以,myprint显示的结果也中断了。
    针对一个线程,一旦调用了detach,就不可以再调用join了,否则会导致程序运行异常。
    detach会导致程序员失去对线程的控制。所以在多数实际项目中,join更为常用,因为毕竟多数情况下程序员需要控制线程的生命周期,而创建一个线程并扔到后台不管的情况比较少。

(4)joinable。

    判断是否可以成功使用join或者detach。
    如果调用了join或者detach,那么joinable会变成false;否则,joinable返回的是true。

{
    thread mytobj(myprint);
    if (mytobj.joinable())
    {
        cout << "1:joinable() == true" << endl;  //成立
    }
    else
    {
        cout << "1:joinable() == false" << endl;
    }
    mytobj.join();  //无论这里调用join()还是detach(),后续的joinable()都会返回false
    if (mytobj.joinable())
    {
        cout << "2:joinable() == true" << endl;
    }
    else
    {
        cout << "2:joinable() == false" << endl; //成立
    }
}

    所以,joinable有一定的用处——判断针对某个线程是否调用过join或者detach:

if(mytobj.joinable())
{
    mytobj.join();
}

  2.2 其他创建线程的写法

(1)用类来创建线程。

class TA
{
public:
    void operator()()  //不带参数
    {
        cout << " TA::operator()开始执行了" << endl;
        //....
        cout << " TA::operator()执行结束了" << endl;
    }
};
//在main主函数中,加入如下代码
{
    TA ta;
    thread mytobj3(ta); //ta,可调用对象,ta:这里不可以是临时对象thread mytobj3(TA()); 否则编译无法通过
    mytobj3.join(); //为保证等待线程执行结束,这里使用join.
    cout << "main主函数执行结束!" << endl;
}

在这里插入图片描述
    执行起来,结果一切正常。另外,类与detach结合使用可能会带来意外问题。
    继续修改TA类:

class TA
{
public:
    TA(int& i) :m_i(i) {}
    void operator()()  
    {
        cout << "mi1的值为:" << m_i << endl; //隐患,m_i可能没有有效值
        cout << "mi2的值为:" << m_i << endl;
        cout << "mi3的值为:" << m_i << endl;
        cout << "mi4的值为:" << m_i << endl;
        cout << "mi5的值为:" << m_i << endl;
        cout << "mi6的值为:" << m_i << endl;
    }
    int& m_i; //引入一个引用类型的成员变量
};
//在main主函数中,加入如下代码
{
    int myi = 6;
    TA ta(myi);
    thread mytobj3(ta);  //创建并执行子线程
    mytobj3.join();
    //mytobj3.detach();
    cout << "main主函数执行结束!" << endl;
}

    请注意,在类TA中,成员变量m_i是一个引用,绑定的是main主函数中的myi变量。所以,当主线程执行结束,很可能子线程在后台在继续运行,但是主线程结束时,myi会被销毁,子线程仍旧使用已经销毁的myi,产生不可预料的后果。这里读者可以通过打印类TA中m_i的地址来确定和main主函数中的myi地址相同来证明类TA中的成员变量m_i绑定的是main主函数中的myi。
    当然,还有个疑问:一旦在main主函数中detach,那么主线程执行结束后,main主函数中的ta对象会被销毁,那么,子线程中看起来正在使用这个ta对象,如果被主线程销毁,是否会出现问题呢?其实,ta对象是会被复制到子线程中。所以,虽然执行完主线程后,ta对象被销毁,但复制到子线程中的对象依旧存在,所以这不是问题。但是,这个对象中如果有引用或者指针,那就另当别论了,那就可能产生问题。

    为了进一步演示,给TA类的构造函数增加一行输出语句,并增加public修饰的析构函数和拷贝构造函数:

{
public:
    TA(int i) :m_i(i) {
        printf("TA()构造函数执行,m_i=%d,this=%p\n", m_i, this);
    }
    ~TA() {
        printf("~TA()析构函数执行,m_i=%d,this=%p\n", m_i, this);
    }
    TA(const TA& ta) :m_i(ta.m_i) {
        printf("TA()拷贝构造函数执行,m_i=%d,this=%p\n", m_i, this);
    }
}

在这里插入图片描述
    从结果不难看到,先释放复制到线程里面去的ta对象(注意this值),因为main中的代码一直在join行等待子线程执行完毕,子线程执行完,当然会先把子线程的对象释放(析构),然后最后一行释放的才是主线程的ta对象。请读者注意比较构造和析构函数输出结果中的this值,这样就能够正确地匹配构造和析构函数的输出结果行。

(2)用lambda表达式来创建线程。

{
    auto mylamthread = [] {
        cout << "我的线程开始执行了" << endl;
        //...
        cout << "我的线程执行完毕了" << endl;
    };
    thread mytobj4(mylamthread);
    mytobj4.join();
    cout << "main主函数执行结束!" << endl;
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值