技术交流QQ群:1027579432,欢迎你的加入!

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

  • 可执行程序运行起来,生成一个进程,该进程所属的主线程开始自动运行。

    #include <iostream>
    #include <vector>
    #include <map>
    #include <string>
    
    
    using namespace std;
    
    int main(){
        
        cout << "I love China!" << endl;  // 实际上这个是主线程在执行,主线程从main()函数返回,则整个进程执行完毕。
        return 0;
    }
    
  • 主线程从main()函数开始执行,当我们自己创建的线程,也需要从一个函数开始运行(初始函数),一旦这个函数运行完毕,就代表我们这个线程运行结束

  • 整个进程是否执行完毕的标志是:主线程是否执行完,如果主线程执行完毕了,就代表整个进程执行完毕了;此时,一般情况下:如果其他子线程还没有执行完毕,那么这些子线程也会***作系统强行终止。所以,一般情况下,如果大家想保持子线程(自己用代码创建的线程)的运行状态的话,那么大家要让主线程一直保持运行,不要让主线程运行结束。==这条规律有例外,后续会解释这种例外!==

1.1 创建线程

  • 主要流程如下
    • 包含一个头文件thread进来
    • 初始函数要写
    • 在main()函数中开始写代码
    #include <iostream>
    #include <thread>
    
    using namespace std;
    
    // 自己创建的线程也要从一个函数(初始函数)开始运行
    void myprint()
    {
        cout << "我的线程开始执行了\n";
        /*
            中间包含其他的业务逻辑代码
        */
        cout << "我的线程执行完毕了\n";
    }
    
    int main(){
        thread mytobj(myprint);
        mytobj.join();
    
        cout << "I love China!" << endl;
        return 0;
    }
    
  • 观察上面的程序结果发现:有两个线程在运行,相当于整个程序的执行有两条线在同时走。所以可以同时干两件事。即使一条线被堵住了,另外一条线仍然是可以通行的,这就是多线程。
    多线程

1.2 thread

  • thread:是标准库中的类
    // myprint是可调用对象
    thread mytobj(myprint);
    
  • 上面的代码做了两件事情
    • a.创建了线程,线程执行起点(入口)myprint();
    • b.myprint线程开始执行

1.3 join()

  • join():加入/汇合,说白了就是阻塞,阻塞主线程,让主线程等待子线程执行完毕,然后子线程和主线程汇合。然后主线程再继续往下走!
    thread mytobj(myprint); 
    mytobj.join(); 
    
  • 上面的代码分析:主线程阻塞到这里等待myprint子线程执行完,当子线程执行完毕,这个join()就执行完毕,主线程再继续往下执行。阻塞主线程并等待myprint()子线程执行完
  • 如果主线程执行完毕,但子线程没执行完毕,这样的程序是不合格的!程序也是不稳定的;一个书写良好的程序,应该是主线程等待子线程执行完毕后,主线程才能最终退出

1.4 detach()

  • 传统多线程的程序主线程要等待子线程执行完毕,然后自己再最后退出。
  • detach():分离,也就是主线程不和子线程汇合了,你主线程执行你的,我子线程执行我的,你主线程也不必等我子线程运行结束,你可以先执行结束,这并不影响我子线程的执行
    thread mytobj(myprint);
    mytobj.detach();
    
  • 为什么会引入detach函数?
    • 我们创建了很多子线程,让主线程逐个等待子线程结束,这种编程方法不太好,所以引入detach()。
  • 一旦detach()之后,与这个主线程关联的thread对象就会失去与这个主线程的关联。此时这个子线程就会驻留在后台运行了(主线程与该子线程失去联系),这个子线程就相当于被C++运行时库接管。当这个子线程执行完毕后,由运行时库负责清理该线程相关的资源(守护线程)
  • detach()使线程myprint失去我们自己的控制。一旦调用了detach(),就不能再用join(),否则系统会报告异常

1.5 joinable()

  • 判断是否可以成功使用join()或者detach();返回true(可以join或者detach)或false(不能join或detach)!
        thread mytobj(myprint); 
        if (mytobj.joinable())
        {
            cout << "1:joinable() == true" << endl;
        }
        else
        {
            cout << "1:joinable() == false" << endl;
        }
    
        mytobj.detach(); // 阻塞主线程并等待myprint()子线程执行完
    
        if (mytobj.joinable())
        {
            cout << "2:joinable() == true" << endl;
        }
        else
        {
            cout << "2:joinable() == false" << endl;
        }
    

2.其他创建线程的手法

2.1 用类对象(可调用对象)以及一个问题的范例

  • 用类创建一个线程的范例如下:
    #include <iostream>
    #include <thread>
    
    using namespace std;
    
    
    class TA
    {
    public:
        void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
        {
            cout << "我的线程operator()开始执行了\n";
            /*
                中间包含其他的业务逻辑代码
            */
            cout << "我的线程operator()结束执行了\n";
        }
    };
    
    int main(){
        
        
        
        TA ta;  // ta是一个类的对象
        thread mytobj3(ta); // 同时,ta也是一个可调用对象
        mytobj3.join(); // 等待子线程执行结束
    
        cout << "I Love China" << endl;
    
        return 0;
    }
    
  • 一个值得注意的问题如下:
    #include <iostream>
    #include <thread>
    
    
    using namespace std;
    
    
    class TA
    {
    public:
        int &m_i;
        TA(int& i) : m_i(i){}
        void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
        {
            cout << "m_i1的值为: " << m_i << endl; // 产生不可意料的结果!
            cout << "m_i2的值为: " << m_i << endl;
            cout << "m_i3的值为: " << m_i << endl;
            cout << "m_i4的值为: " << m_i << endl;
            cout << "m_i5的值为: " << m_i << endl;
            cout << "m_i6的值为: " << m_i << endl;
        }
    };
    
    
    
    int main(){
    
        int myi = 6;
        TA ta(myi);  // ta是一个类的对象
        thread mytobj3(ta); // 同时,ta也是一个可调用对象
        // mytobj3.join(); // 等待子线程执行结束
        mytobj3.detach();
    
        cout << "I Love China" << endl;
        return 0;
    }
    
  • 上述程序的结果分析:由于使用mytobj3.detach(),因此主线程与子线程之间是无关的。主线程执行主线程的,子线程执行子线程的。现在假设出现一种情况,主线程先执行完了,子线程后执行完的。由于子线程的函数中使用的是引用成员变量m_i,m_i是与形参引用变量i绑定在一起的。而形参变量i是主线程中实参myi的引用,myi是主线程中的局部变量,当主线程执行结束后,变量myi的内存空间就会被系统所回收。但是,子线程仍然没有执行完,还在打印已经被销毁的变量m_i的内容,那么就会产生不可意料的结果了
  • 对上述程序还有一个疑问:一旦调用了detach(),当主线程执行结束了,主线程中用的这个ta对象还存在吗?答:这个ta对象已经不存在了,因为这个对象ta实际上是被复制到子线程中去的!所以执行完主线程后,ta对象会被销毁,但是所复制的ta对象依旧存在。所以,只要你这个TA类对象里没有引用、没有引用,那么就不会产生问题。验证程序如下所示:
    #include <iostream>
    #include <thread>
    
    
    using namespace std;
    
    
    class TA
    {
    public:
        int &m_i;
        TA(int& i) : m_i(i){
            cout << "TA构造函数被执行" << endl;
        }
    
        TA(const TA& ta) : m_i(ta.m_i)
        {
            cout << "TA()拷贝构造函数被执行" << endl;
        }
    
        ~TA()
        {
            cout << "TA()析构函数被执行" << endl;
        }
    
        void operator()() // 类对象变成可调用对象,即后面的括号中不能带参数
        {
            cout << "m_i1的值为: " << m_i << endl;
            cout << "m_i2的值为: " << m_i << endl;
            cout << "m_i3的值为: " << m_i << endl;
            cout << "m_i4的值为: " << m_i << endl;
            cout << "m_i5的值为: " << m_i << endl;
            cout << "m_i6的值为: " << m_i << endl;
        }
    };
    
    
    
    int main(){
    
        int myi = 6;
        TA ta(myi);  // ta是一个类的对象
        thread mytobj3(ta); // 同时,ta也是一个可调用对象
        // mytobj3.join(); // 等待子线程执行结束
        mytobj3.detach();
    
        cout << "I Love China" << endl;
        return 0;
    }
    
结果分析

2.2 用lambda表达式创建线程

  • 实例如下:
    int main(){
    
        auto mylamthread = []
        {
            cout << "我的线程3开始执行了" << endl;
            // ...
            cout << "我的线程3执行结束了" << endl;
        };
    
        thread mytobj4(mylamthread);
        mytobj4.join();
        return 0;
    }