从Qt控制摄像头:OS线程与事件循环的交互

时间:2012-05-22 15:47:25

标签: multithreading qt camera sleep

我正在研究Qt应用程序来控制工业相机,特别是我需要在特定时间触发相机(例如,当各种照明设置到位时),并等到一帧退回。在最简单的情况下,以下代码可以很好地完成工作:

void AcquireFrame()
{
    // Runs in the main GUI thread:
    camera -> m_mutex.lock();
    camera -> frameHasArrived = false;
    camera -> m_mutex.unlock();
    camera -> triggerImageAcquisition();

    forever
    {
        camera -> m_mutex.lock()
        bool isReady = camera -> frameHasArrived;
        camera -> m_mutex.unlock()
        if (isReady)
        {
            return;
        }
        else
        {
            Sleep(10);
        }
    }
}

void callback(camera *)
{
    // Called by the camera driver from a separate OS thread - not a Qt thread - 
    // when a frame is ready:
    camera -> m_mutex.lock();
    camera -> frameHasArrived = true;
    camera -> m_mutex.unlock();
}

......而且大部分时间这都非常有效。然而,这是现实世界,偶尔相机将无法接收触发器或计算机将无法完全接收帧,上面的代码将进入无限循环。

显而易见的事情是暂停,所以如果在一定时间内没有收到帧,则可以再次尝试图像采集。修改后的代码如下:

void AcquireFrame()
{
    camera -> m_mutex.lock();
    camera -> frameHasArrived = false;
    camera -> m_mutex.unlock();
    camera -> triggerImageAcquisition();

    QTime timeout;
    timeout.start();
    forever
    {
        timeout.restart();

fetch:  camera -> m_mutex.lock()
        bool isReady = camera -> frameHasArrived;
        camera -> m_mutex.unlock()
        if (isReady)
        {
            return;
        }
        else if (timeout.elapsed() > CAM_TIMEOUT) 
        { 
            // Assume the first trigger failed, so try again:
            camera -> triggerImageAcquisition();
            continue;
        }
        else
        {
            Sleep(10);
            goto fetch;
        }
    }
}

现在,问题在于,对于后一版本,失败率(“不成功的触发器”的比例)要高得多,至少要高一个数量级。此外,这个代码也最终会发现自己处于一个无限循环中,无论多少次它试图重新触发相机,它都不会看到一个帧回来。在后一种情况下,杀死应用程序并检查相机会发现相机处于完好的工作状态并耐心地等待下一次触发,因此它似乎不是相机问题。我得出的结论是,实际上它是某种系统资源问题或线程冲突,因此Qt的事件循环不允许在适当的时间调用相机回调。

这是否可能,实际上有更好的方法吗?


6月6日更新:

为了它的价值,自从我采用下面的方法(给相机对象增加一个成员,即一个名为'm_condition'的QWaitCondition)后,我再也没有看到任何问题:

void AcquireFrame()
{
    bool frameReceived;

    forever
    {
        camera -> triggerImageAcquisition();

        camera -> m_mutex.lock();
        frameReceived = camera -> m_condition.wait(&camera->m_mutex, CAM_TIMEOUT);

        if (frameReceived)
        {
            // We received a frame from the camera, so can return:
            camera -> m_mutex.unlock();
            return;
        }

        // If we got to here, then the wait condition must have timed out. We need to
        // unlock the mutex, go back to the beginning of the 'forever' loop and try 
        // again:
        camera -> m_mutex.unlock();
    }
}

void callback (camera *)
{
    // Called by the camera driver from a separate OS thread -
    // not a QThread - when a frame is ready:
    camera -> m_condition.wakeOne();
}

这仍然具有暂停主线程的效果,直到我们接收到帧或经历超时,但现在我们已经消除了Sleep()并且Qt事件循环始终保持完全控制。我仍然不清楚为什么旧方法会导致很多问题 - 我仍然怀疑某种系统资源有限 - 但这种新方法看起来更轻巧,当然效果更好。

2 个答案:

答案 0 :(得分:1)

在GUI线程中运行阻塞在互斥锁上的AcquireFrame对我来说没有多大意义,除非你想要牺牲GUI重复性来延迟延迟,但我怀疑你是否关心延迟,因为相机单拍框架,你坚持在繁忙的GUI线程中处理它们。

其次,Qt没有做任何事情来防止从另一个线程调用回调,除了优先级较低的另一个线程被高优先级线程抢占完全垄断CPU。

我只是在回调函数中将事件发布到GUI线程(或任何其他QThread!)中的QObject。你可以从任何线程发布事件,没关系 - 重要的是接收器。毕竟,QCoreApplication::postEvent是一个静态函数,它根本不检查当前线程。

在复杂的应用程序中,您可能希望将逻辑放在专用控制器QObject中,而不是分布在QWidget派生类中。所以你只需将事件发布到控制器实例。

请注意,将事件发布到空闲GUI线程将与使用互斥锁完全相同 - Qt的事件循环使用互斥锁并在该互斥锁和操作系统的消息上休眠。美妙的是,Qt已经在等着你,但是等待是可以中断的。发布的事件应具有高优先级,以便它将结束队列中的第一个事件并抢占所有其他事件。当您准备好获取帧时,但在触发帧之前,您可以调用QCoreApplication::flush()。就是这样。

使用专用QThread中的单独图像处理器QObject来利用多核机器应该没有问题。然后,您可以将图像处理为QImage,并使用其他事件将其转发到GUI线程,或者仅通过信号槽连接。您可能还可以在获取帧时通知GUI线程,但只是开始处理它。这样一来,用户就会发生一些事情变得更加明显。如果图像处理需要很长时间,您甚至可以发送定期更新,这些更新将映射到进度条。

基准测试结果(使用发布版本)很有趣,但符合Qt的事件队列在内部受互斥锁保护的事实,并且事件循环在该互斥锁上等待。哦,结果似乎可以在mac和windows xp平台之间移植。

使用裸露的等待条件不是最快的方式,但使用裸露的事件甚至更慢。最快的方法是使用排队的信号槽连接。在这种情况下,将事件发布到同一个线程(即FrameProcessorEvents::tick()所做的事情)的成本似乎可以忽略不计。

的Mac

warming caches...
benchmarking...
wait condition latency: avg=45us, max=152us, min=8us, n=1001
queued signal connection latency: avg=25us, max=82us, min=10us, n=1000
queued event latency: avg=71us, max=205us, min=14us, n=1000

VMWare Fusion下的Windows XP

请注意,超过1毫秒的结果可能是由于此时未安排VMWare。

warming caches...
benchmarking...
wait condition latency: avg=93us, max=783us, min=8us, n=1000
queued signal connection latency: avg=46us, max=1799us, min=0us, n=1000
queued event latency: avg=117us, max=989us, min=18us, n=1001

以下是基准测试代码。

#include <cstdio>
#include <limits>
#include <QtCore>

QTextStream out(stdout);

class TimedBase : public QObject
{
public:
    TimedBase(QObject * parent = 0) : QObject(parent) { reset(); }
    friend QTextStream & operator<<(QTextStream & str, const TimedBase & tb) {
        return str << "avg=" << tb.avg() << "us, max=" << tb.usMax << "us, min="
                   << tb.usMin << "us, n=" << tb.n;
    }
    void reset() { usMax = 0; n = 0; usMin = std::numeric_limits<quint32>::max(); usSum = 0; }
protected:
    quint64 n, usMax, usMin, usSum;
    quint64 avg() const { return (n) ? usSum/n : 0; }
    void tock() {
        const quint64 t = elapsed.nsecsElapsed() / 1000;
        usSum += t;
        if (t > usMax) usMax = t;
        if (t < usMin) usMin = t;
        n ++;
    }
    QElapsedTimer elapsed;
};

class FrameProcessorEvents : public TimedBase
{
    Q_OBJECT
public:
    FrameProcessorEvents(QObject * parent = 0) : TimedBase(parent) {}
public slots: // can be invoked either from object thread or from the caller thread
    void tick() {
        elapsed.start();
        QCoreApplication::postEvent(this, new QEvent(QEvent::User), 1000);
    }
protected:
    void customEvent(QEvent * ev) { if (ev->type() == QEvent::User) tock(); }
};

class FrameProcessorWait : public TimedBase
{
    Q_OBJECT
public:
    FrameProcessorWait(QObject * parent = 0) : TimedBase(parent) {}
    void start() {
        QTimer::singleShot(0, this, SLOT(spinner()));
    }
public: // not a slot since it must be always invoked in the caller thread
    void tick() { elapsed.start(); wc.wakeAll(); }
protected:
    QMutex mutex;
    QWaitCondition wc;
protected slots:
    void spinner() {
        forever {
            QMutexLocker lock(&mutex);
            if (wc.wait(&mutex, 1000)) {
                tock();
            } else {
                return;
            }
        }
    }
};

FrameProcessorEvents * fpe;
FrameProcessorWait * fpw;

static const int avgCount = 1000;
static const int period = 5;

class FrameSender : public QObject
{
    Q_OBJECT
public:
    FrameSender(QObject * parent = 0) : QObject(parent), n(0), N(1) {
        QTimer::singleShot(0, this, SLOT(start()));
    }
protected slots:
    void start() {
        out << (N ? "warming caches..." : "benchmarking...") << endl;
        // fire off a bunch of wait ticks
        n = avgCount;
        timer.disconnect();
        connect(&timer, SIGNAL(timeout()), SLOT(waitTick()));
        fpw->reset();
        fpw->start();
        timer.start(period);
    }
    void waitTick() {
        fpw->tick();
        if (!n--) {
            if (!N) { out << "wait condition latency: " << *fpw << endl; }
            // fire off a bunch of signal+event ticks
            n = avgCount;
            fpe->reset();
            timer.disconnect();
            connect(&timer, SIGNAL(timeout()), fpe, SLOT(tick()));
            connect(&timer, SIGNAL(timeout()), SLOT(signalTick()));
        }
    }
    void signalTick() {
        if (!n--) {
            if (!N) { out << "queued signal connection latency: " << *fpe << endl; }
            // fire off a bunch of event-only ticks
            n = avgCount;
            fpe->reset();
            timer.disconnect();
            connect(&timer, SIGNAL(timeout()), SLOT(eventTick()));
        }
    }
    void eventTick() {
        fpe->tick();
        if (!n--) {
            if (!N) { out << "queued event latency: " << *fpe << endl; }
            if (!N--) {
                qApp->exit();
            } else {
                start();
            }
        }
    }

protected:
    QTimer timer;
    int n, N;
};

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    QThread eThread;
    QThread wThread;
    eThread.start(QThread::TimeCriticalPriority);
    wThread.start(QThread::TimeCriticalPriority);
    fpw = new FrameProcessorWait();
    fpe = new FrameProcessorEvents();
    fpw->moveToThread(&eThread);
    fpe->moveToThread(&wThread);
    FrameSender s;
    a.exec();
    eThread.exit();
    wThread.exit();
    eThread.wait();
    wThread.wait();
    return 0;
}

#include "main.moc"

答案 1 :(得分:0)

检测触发状态并开启相机需要做多少工作? 如果这相对便宜 - 我会有一个单独的线程只是阻止触发事件并触发相机。然后通过回调函数发送的Qt信号通知主线程。