Qt5:如何在线程中等待一个信号?

28

可能这个标题的问题不是很明确。我正在Windows7上使用Qt5。

在一个线程(QThread)中, 在某一点,我必须等待属于QSslSocket的"encrypted()"信号,在这个线程中我正在使用它。同时我想使用QTimer并等待"timeout()"信号,以避免被阻塞在无限循环中...
现在我拥有的是:


在一个线程(QThread)中,在"process()"函数或方法的某一点,我必须等待属于QSslSocket的"encrypted()"信号,这个QSslSocket是在该线程中使用的。此外,我认为我应该使用QTimer并等待"timeout()"信号,以避免被阻塞在无限循环中... 目前我有:
// start processing data
void Worker::process()
{
    status = 0;
    connect(sslSocket, SIGNAL(encrypted()), this, SLOT(encryptionStarted()));
    QTimer timer;
    connect(&timer, SIGNAL(timeout()), this, SLOT(timerTimeout()));
    timer.start(10000);
    while(status == 0)
    {
        QThread::msleep(5);
    }

    qDebug("Ok, exited loop!");

    // other_things here
    // .................
    // end other_things

    emit finished();
}

// slot (for timer)
void Worker::timerTimeout()
{
    status = 1;
}

// slot (for SSL socket encryption ready)
void Worker::encryptionStarted()
{
    status = 2;
}

很明显它不起作用,它将永远停留在那个while循环中...
所以问题是:有没有一种方法来解决这个问题?如何等待那个"encrypted()"信号但不超过 - 比如说10秒钟 - 以避免被困在等待循环/线程中?

5个回答

81

你可以使用本地事件循环等待信号的发出:

QTimer timer;
timer.setSingleShot(true);
QEventLoop loop;
connect( sslSocket, &QSslSocket::encrypted, &loop, &QEventLoop::quit );
connect( &timer, &QTimer::timeout, &loop, &QEventLoop::quit );
timer.start(msTimeout);
loop.exec();

if(timer.isActive())
    qDebug("encrypted");
else
    qDebug("timeout");

在这里等待,直到encrypted事件被触发或超时时间到达。


1
非常好的想法。我进行了测试(对其进行了一些修改以适应我的代码和目的),它可以正常工作。当然我投了赞成票 :) - סטנלי גרונן
使用事件循环这种方式会导致“忙等待”,对吗? - harihardik
1
@harihardik 不是的。在等待主线程中的所有事件被处理并且GUI正常工作。但这种编码方式不是好的实践,建议使用异步方法。 - Nejat
1
@Pa_ 您可以将 encrypted 信号简单地连接到一个带有参数的 lambda 中,并在 lambda 中调用事件循环的 quit 方法(lambda 中应该捕获对事件循环对象的引用)。 - Nejat
我需要担心“断开连接”问题吗?无论是使用“lambda”还是“quit”方式。 - undefined
显示剩余2条评论

11

从Qt 5.0开始,QSignalSpy提供了一个wait方法。将其连接到信号并调用wait()会阻塞直至信号发出。

QSignalSpy spy(SIGNAL(encrypted()));
spy.wait(5000);  //wait until signal fires or 5 second timeout expires

2
请注意,这个类包含在Qt::Test模块中。 - ManuelSchneid3r

8
在异步编程中,“等待”被认为是一种反模式。不要等待事情发生,而是设计代码以响应条件的实现。例如,将代码连接到信号。一种实现方式是将操作分成单独的状态,并在进入每个状态时进行一些工作。当然,如果工作量不可忽略,请使用单独的槽而不是lambda表达式来保持可读性。
请注意显式内存管理的缺失。对Qt类使用拥有指针是一种过早优化,应避免不必要的情况。对象可以是Worker(或其PIMPL)的直接成员。
子对象必须全部属于以Worker为根的所有权层次结构。这样,您可以安全地将Worker实例移动到另一个线程,并且它所使用的对象也会跟随移动。当然,您也可以在正确的线程中实例化Worker——有一个简单的习语可以做到这一点。线程的事件分发器拥有该工作者,因此当线程的事件循环退出(即调用QThread::quit()之后),该工作者将自动释放,不会泄漏任何资源。
template <typename Obj>
void instantiateInThread(QThread * thread) {
  Q_ASSERT(thread);
  QObject * dispatcher = thread->eventDispatcher();
  Q_ASSERT(dispatcher); // the thread must have an event loop
  QTimer::singleShot(0, dispatcher, [dispatcher](){
    // this happens in the given thread
    new Obj(dispatcher);
  });
}

工作线程的实现:
class Worker : public QObject {
  Q_OBJECT
  QSslSocket sslSocket;
  QTimer timer;
  QStateMachine machine;
  QState s1, s2, s3;
  Q_SIGNAL void finished();
public:
  explicit Worker(QObject * parent = {}) : QObject(parent),
    sslSocket(this), timer(this), machine(this),
    s1(&machine), s2(&machine), s3(&machine) {
    timer.setSingleShot(true);
    s1.addTransition(&sslSocket, SIGNAL(encrypted()), &s2);
    s1.addTransition(&timer, SIGNAL(timeout()), &s3);
    connect(&s1, &QState::entered, [this]{
      // connect the socket here
      ...
      timer.start(10000);
    });
    connect(&s2, &QState::entered, [this]{
      // other_things here
      ...
      // end other_things
      emit finished();
    });
    machine.setInitialState(&s1);
    machine.start();
  }
};

然后:

void waitForEventDispatcher(QThread * thread) {
  while (thread->isRunning() && !thread->eventDispatcher())
    QThread::yieldCurrentThread();
}

int main(int argc, char ** argv) {
  QCoreApplication app{argc, argv};
  struct _ : QThread { ~Thread() { quit(); wait(); } thread;
  thread.start();
  waitForEventDispatcher(&thread);
  instantiateInThread<Worker>(&myThread);
  ...
  return app.exec();
}

请注意,连接到QThread::started()将存在竞争条件:事件分派程序直到QThread::run()中的某些代码有机会执行之前才存在。因此,我们必须通过yield等待线程到达那里——这很可能使工作线程在一两个yield内进展得足够远。因此,它不会浪费太多时间。

1
非常有趣的方法!我明天会测试它(在适应我的代码之后),并尝试提供相关反馈 :) 嗯,在线程内部拥有状态机真是太棒了!抱歉我这么热情,但我是Qt的新手,所以每次看到新东西(对我来说)时,我都意识到Qt/C++可以提供多么巨大的可能性...!我希望能看到更多像这样的例子!我该说什么?太棒了,绝妙的想法! - סטנלי גרונן
1
@Mitch 手动内存管理是指您使用new创建一个对象并将其分配给原始的C指针,然后必须手动删除它。代码甚至没有使用new进行显式内存分配,但如果有的话,结果将立即分配给智能指针或作为QObject树中的子项。请注意,QObject充当其他QObject的智能指针集合。 - Kuba hasn't forgotten Monica
@Mitch 很多糟糕的Qt示例(包括与Qt捆绑在一起的示例)都使用完全不必要的显式堆分配。由于每个重要的Qt类都分配了一个PIMPL,因此将PIMPL指针放在堆上会使分配数量增加一倍。即使其PIMPL大两个数量级,QObjectQWidget的大小也约为4-6个指针。如果您在构造函数中添加子项,请将它们作为类或其PIMPL的常规(非指针)成员添加即可。 - Kuba hasn't forgotten Monica
谢谢。我不确定你是指基于QObject的内存管理还是智能指针。 - Mitch
基本上,使用现代C++,你应该让编译器和库来为你管理内存。如果你手动管理内存而且没有用通常的RAII封装,那么你就做错了 :) - Kuba hasn't forgotten Monica
显示剩余4条评论

4

这些天我有些时间,进行了一些调查...
好吧,我浏览了"http://doc.qt.io/qt-5/qsslsocket.html"并发现了以下内容:

bool QSslSocket::waitForEncrypted(int msecs = 30000)

真丢脸,我之前没注意到……:(
一定要买点眼镜(可惜,这不是个玩笑!)
我愿意根据需要修改我的代码来测试它(周一在办公室)。
很有可能它会起作用。
[晚更新]:
是的,这是我在最终代码中实现的解决方案,它很好地运行,所以我决定分享 :)


0

2
展示一个例子可以提高这篇文章的长期价值。 - starball
1
你的回答可以通过提供更多支持信息来改进。请编辑以添加进一步的细节,例如引用或文档,以便他人可以确认你的答案是正确的。您可以在帮助中心找到有关如何编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接