【QT】解决继承QThread的子线程导致程序无法关闭&主线程关闭太快导致子线程中的槽方法未执行

41

背景

使用串口进行通信

  • 一共有三个线程
    • 主线程负责界面的显示
    • 子线程1负责检测当前系统可用的串口
    • 子线程2负责差串口通信

子线程实现

  • 在发生问题的最初,因为要一直检测当前系统的可用线程,所以线程1我使用继承自QThread实现的线程,其中重写run函数,并添加while循环,详见问题1中的代码。发生问题所在
  • 子线程2使用movetoThread实现,问题不再这里出现,略。

Q1: 继承QThread的子线程导致程序无法关闭

源代码

产生错误的代码

  • 子线程的run函数
void Check_Serial_Monitor_Thread::run()
{
    m_odd_serial_list.clear();
    QStringList tmp_str_list;

    while(open_flag){// 不断检测是否有新的可用串口出现
        tmp_str_list.clear();
        // 可以再详细一下,检测链接进来然后发生断开的情况。暂时未实现
        foreach (const QSerialPortInfo &info, QSerialPortInfo::availablePorts()) {
            tmp_str_list += info.portName();
        }
        if(tmp_str_list != m_odd_serial_list){// 更新下拉框
            m_odd_serial_list = tmp_str_list;// 更新之前的保存的数据
            Custom_Tools::Print("Update available serial port!");
            emit Have_Change_Serial(tmp_str_list);
        }
    }
}
  • 主线程构造函数中链接信号与槽
connect(this,&Widget::Stop_Serial_Monitor_Thread,
        m_check_serial_thread,&Check_Serial_Monitor_Thread::Stop_Cur_Thread,
        Qt::QueuedConnection);
  • 子线程中的槽方法
void Check_Serial_Monitor_Thread::Stop_Cur_Thread()
{
    Custom_Tools::Print("Quit Slot");
    open_flag = false;
}
  • 主线程析构函数中发出信号
Widget::~Widget()
{
    emit Stop_Serial_Monitor_Thread();

    // 将串口关闭
    if(ui->operate_serial_switch_btn->text() == QString("关闭")){
        Operator_Serial_Switch();
    }

    m_check_serial_thread->quit();
    m_check_serial_thread->wait();

    m_serial_comm_thread.quit();
    m_serial_comm_thread.wait();

    delete ui;
}

问题产生

  • 关闭主窗口后,发现程序并未退出。

错误解析 & 心路历程

原因猜测

事实证明,我的猜想是正确的。

  • 我将第五个参数改为了Qt::BlockingQueuedConnection
    // 关闭串口检测线程的信号
    connect(this,&Widget::Stop_Serial_Monitor_Thread,
            m_check_serial_thread,&Check_Serial_Monitor_Thread::Stop_Cur_Thread,
            Qt::BlockingQueuedConnection);
  • 果不其然,程序发生了死锁,正如官方文档中所说。

Qt::BlockingQueuedConnection Same as Qt::QueuedConnection, except that the signalling thread blocks until the slot returns. This connection must not be used if the receiver lives in the signalling thread, or else the application will deadlock.

与 Qt::QueuedConnection 相同,除了信号线程阻塞直到槽返回。 如果接收器位于信号线程中,则不得使用此连接,否则应用程序将死锁。

  • 也就是说,如果添加第五个参数,指定的槽方法执行方式,还是对于主线程来说的。因为这的对象属于主线程。
    • 事件循环,以及事件这个机制是对于线程来说的,而不是对象。
    • 补充: Per-Thread Event Loop
  • OK,现在问题很明确了,为什么这个子线程退不出去?
    • 就是因为run函数中的while(open_flag)没有被更改为false从而终止循环。
    • 为什么没被更改?
      • 因为我们的信号对应的槽函数没有被执行?
    • 为什么没被执行?
      • 因为使用参数Qt::QueuedConnection被放到了主线程的事件队列中,等待当前代码执行完毕之后被执行.

解决方式

  • 在该发送信号后手动调用事件处理。即,先处理这个。
  • 或者去掉Qt::QueuedConnection。
emit Stop_Serial_Monitor_Thread();
QApplication::processEvents();
  • 因为上面run函数没有被终止,进一步导致下方wait函数阻塞,使程序无法终止。
  • 对于quit函数来说——void QThread::quit()

Tells the thread's event loop to exit with return code 0 (success). Equivalent to calling QThread::exit(0). This function does nothing if the thread does not have an event loop.

告诉线程的事件循环退出,返回代码为0(成功)。相当于调用QThread::exit(0)。 如果线程没有事件循环,此函数将不执行任何操作。

Blocks the thread until either of these conditions is met: The thread associated with this QThread object has finished execution (i.e. when it returns from run()). This function will return true if the thread has finished. It also returns true if the thread has not been started yet. The deadline is reached. This function will return false if the deadline is reached. A deadline timer set to QDeadlineTimer::Forever (the default) will never time out: in this case, the function only returns when the thread returns from run() or if the thread has not yet started. This provides similar functionality to the POSIX pthread_join() function.

阻塞线程,直到满足以下任一条件: 与此QThread对象关联的线程已完成执行(即,当它从run()返回时)。如果线程已完成,此函数将返回true。如果线程还没有启动,它也会返回true。 截止日期已到。如果达到截止日期,此函数将返回false。

  • 那么对于继承自QThread实现的线程来说,重写run函数,当并未开启事件循环时,如上面的代码所示,当run函数结束后,线程已经结束了(我是这么认为的)。
    • 加上实际上我们并没有事件循环,quit也不会进行任何操作。
    • 否则,貌似会给当前线程添加一个终止事件,当事件循环执行到这个时,退出循环并结束线程。

结束

  • 至此,导致该程序无法正常退出的问题已经解决,但是,也只是可以让程序正常退出,从我们程序的目的来看,还是要使用moveToThread来创建子线程。
    • 使得我们的子线程具有更多的功能,比如——信号与槽。将某些东西让其在子线程中运行。

Q2:主线程关闭太快导致子线程中的槽方法未执行

背景

  • 我将Q1中出现问题的线程重写,采用moveToThread的方法将对应移动到子线程中,在子线程中开启一个定时器,超时就去检测可用串口。
  • 同样在主线程的析构函数中发出信号,对应的槽方法为停止这个子线程中的定时器。

问题产生

  • 程序可以退出,但是发现对应的子线程中的槽方法并未执行。

错误解析

这里感谢下韬哥,带着我一起调试,解决了这个困扰了我几天的问题。

涉及到的代码

  • 主线程析构函数
Widget::~Widget()
{

    emit Stop_Serial_Monitor_Thread();

    // 终止串口检测线程信号
    // 将串口关闭
    if(ui->operate_serial_switch_btn->text() == QString("关闭")){
        Operator_Serial_Switch();
    }

    m_check_serial_thread.quit();
    m_check_serial_thread.wait();

    m_serial_comm_thread.quit();
    m_serial_comm_thread.wait();

    delete ui;
}
  • 子线程中对应的槽方法
void Check_Serial_Monitor_Worker::Stop_Cur_Thread()
{
    Custom_Tools::Print("Check Serial Stop");
    m_timer->stop();
}

解决

  • 在析构函数中,在该信号发送后,Sleep阻塞主线程一下,让他结束慢点,发现该槽方法成功调用。
  • 或者,connect中使用参数Qt::BlockingQueuedConnection,使其在该槽方法执行完毕前,阻塞主线程,直到子线程对应槽方法执行完毕后返回。

补充

  • 总结时发现,调试的时候也可以通过检测这个finished信号,看时间循环时什么时候关闭的。


结束语

  • 其实我省略了一些过程内容,并在总结时整合了部分内容,文章中涉及到的一些细节,我可能还没有细挖,感兴趣的小伙伴可以自行查阅资料,有好的内容可以告诉我。
  • 有错误内容还请及时告诉我,希望能帮助到有需要的小伙伴。