1 调用std::thread::join()方法等待线程退出时的示例问题程序

#include <iostream>
#include "conio.h"

#include <memory>
#include <thread>
#include <atomic>


class BaseThread
{
public:
    typedef std::shared_ptr<BaseThread> ptr;
    BaseThread()
    {
        m_Age = 10;
        m_ChildThreadPtr = nullptr;
        m_bChildStop = false;
    }

    virtual~BaseThread()
    {
        std::cout << "析构" << std::endl;
    }

    void ChildThreadFunc()
    {
        while (!m_bChildStop)
        {
            m_Age++;
            std::cout << "子线程运行" << std::endl;
            std::this_thread::sleep_for(std::chrono::microseconds(3000000));
        }
    }

    void StartThread()
    {
        if (m_ChildThreadPtr != nullptr)
            m_ChildThreadPtr.reset();
        m_ChildThreadPtr = std::make_shared<std::thread>(&BaseThread::ChildThreadFunc, this);
    }

    void StopThread()
    {
        m_bChildStop = true;

        if (m_ChildThreadPtr != nullptr)
        {
            m_ChildThreadPtr->join();
            //if (m_ChildThreadPtr->joinable())
            //{
            //  m_ChildThreadPtr->join();
            //}
        }
    }

private:
    std::atomic<int> m_Age;
    std::shared_ptr<std::thread> m_ChildThreadPtr;
    std::atomic<bool> m_bChildStop;
};


int main()
{
    std::shared_ptr<BaseThread> pBaseThread = std::make_shared<BaseThread>();

    if (pBaseThread != nullptr)
    {
        pBaseThread->StartThread();

        while (true)
        {
            // 在此处填入需要循环的代码

            if (_kbhit()) // 如果有按键被按下
            {
                if (_getch() == 'q') //如果按下了q键则跳出循环
                {
                    std::cout << "退出" << std::endl;

                    // 不小心执行了两次停止线程
                    pBaseThread->StopThread();
                }

            }
        }
    }

    getchar();

    return 0;
}


在上述程序中,我们声明了一个BaseThread类,在该类中我们使用类的成员函数ChildThreadFunc()作为子线程函数,并使用StartThread()开启执行子线程,StopThread()停止执行子线程。

在main()函数中我们先使用StartThread()开启执行子线程,然后再通过按q键调用StopThread()退出子线程。

但是我们一不小心就连续按了两下q键,导致调用了两次StopThread()方法,这个时候程序出现了crash,并出现了如下报错:

C++11 – 使用std::thread::join()/std::thread::detach()方法需要注意的点-StubbornHuang Blog

2 问题原因

在上述main函数中,当我们按下了第一次q键的时候,子程序已经调用了join方法,这导致std::thread对象失去了与之相关联的线程对象,所以当我们再按下了一次q键,发现现在的std::thread对象已经不可join,导致了程序发生了中断。

3 std::thread出现不可join的几种情况

std::thread在以下几种情况下是不可join的:

  • 由std::thread默认构造的std::thread对象,也就是没有指定线程函数的std::thread对象是不可join的;
  • 该std::thread对象被std::move操作
  • 该std::thread对象已经执行过std::thread::join()方法或者std::thread::detach()方法,此时std::thread对象是不可join的;

4 使用std::thread::join()/std::thread::detach()方法需要注意的点

4.1 在执行std::thread::join()/std::thread::detach()方法之前最好判断该std::thread对象是可join的

if (m_ChildThreadPtr->joinable())
{
    m_ChildThreadPtr->join();
}

即第1节中的bug可以如此修复:

#include <iostream>
#include "conio.h"

#include <memory>
#include <thread>
#include <atomic>


class BaseThread
{
public:
    typedef std::shared_ptr<BaseThread> ptr;
    BaseThread()
    {
        m_Age = 10;
        m_ChildThreadPtr = nullptr;
        m_bChildStop = false;
    }

    virtual~BaseThread()
    {
        std::cout << "析构" << std::endl;
    }

    void ChildThreadFunc()
    {
        while (!m_bChildStop)
        {
            m_Age++;
            std::cout << "子线程运行" << std::endl;
            std::this_thread::sleep_for(std::chrono::microseconds(3000000));
        }
    }

    void StartThread()
    {
        if (m_ChildThreadPtr != nullptr)
            m_ChildThreadPtr.reset();
        m_ChildThreadPtr = std::make_shared<std::thread>(&BaseThread::ChildThreadFunc, this);
    }

    void StopThread()
    {
        m_bChildStop = true;

        if (m_ChildThreadPtr != nullptr)
        {
            if (m_ChildThreadPtr->joinable())
            {
                m_ChildThreadPtr->join();
            }
        }
    }

private:
    std::atomic<int> m_Age;
    std::shared_ptr<std::thread> m_ChildThreadPtr;
    std::atomic<bool> m_bChildStop;
};


int main()
{
    std::shared_ptr<BaseThread> pBaseThread = std::make_shared<BaseThread>();

    if (pBaseThread != nullptr)
    {
        pBaseThread->StartThread();

        while (true)
        {
            // 在此处填入需要循环的代码

            if (_kbhit()) // 如果有按键被按下
            {
                if (_getch() == 'q') //如果按下了q键则跳出循环
                {
                    std::cout << "退出" << std::endl;

                    pBaseThread->StopThread();
                }

            }
        }
    }

    getchar();

    return 0;
}

在调用join之前判断joinable,只有在线程对象是可join的情况下再进行join或者detach操作。

4.2 不要忘记对有关联的std::thread对象调用join或者detach方法

这里有关联的指的是已经为该线程指定了线程函数。

例如我们将第一节的程序进行修改,删除按q键退出子程序的代码。

#include <iostream>
#include "conio.h"

#include <memory>
#include <thread>
#include <atomic>


class BaseThread
{
public:
    typedef std::shared_ptr<BaseThread> ptr;
    BaseThread()
    {
        m_Age = 10;
        m_ChildThreadPtr = nullptr;
        m_bChildStop = false;
    }

    virtual~BaseThread()
    {
        std::cout << "析构" << std::endl;
    }

    void ChildThreadFunc()
    {
        while (!m_bChildStop)
        {
            m_Age++;
            std::cout << "子线程运行" << std::endl;
            std::this_thread::sleep_for(std::chrono::microseconds(3000000));
        }
    }

    void StartThread()
    {
        if (m_ChildThreadPtr != nullptr)
            m_ChildThreadPtr.reset();
        m_ChildThreadPtr = std::make_shared<std::thread>(&BaseThread::ChildThreadFunc, this);
    }

    void StopThread()
    {
        m_bChildStop = true;

        if (m_ChildThreadPtr != nullptr)
        {
            if (m_ChildThreadPtr->joinable())
            {
                m_ChildThreadPtr->join();
            }
        }
    }

private:
    std::atomic<int> m_Age;
    std::shared_ptr<std::thread> m_ChildThreadPtr;
    std::atomic<bool> m_bChildStop;
};


int main()
{
    std::shared_ptr<BaseThread> pBaseThread = std::make_shared<BaseThread>();

    if (pBaseThread != nullptr)
    {
        pBaseThread->StartThread();
    }

    getchar();

    return 0;
}

运行上述程序,程序报错:
C++11 – 使用std::thread::join()/std::thread::detach()方法需要注意的点-StubbornHuang Blog

在一个有关联的线程函数的std::thread对象如果没有调用join或者detach方法会在std::thread对象析构的时候中断程序。如果程序发生异常,也必须在异常处理中调用std::thread对象的join或者detach方法。