问题 设计模式,Qt模型/视图和多线程


我正在创建一个显示市场数据的应用程序,并以其他形式使用它。我将市场数据存储在地图上 std::map<tickerId, StockData>。让我举一个例子来说明如何使用这张地图。

  1. 网络在时间t发送封装库存数据的数据包。 updatePrice(tickerId, latestPrice)
  2. 更新地图中的库存数据。现在,多个线程可以访问/更新数据。因此必须锁定映射以进行线程安全操作。这是第一个问题,我是否还需要锁定基础数据以进行更新?
  3. 新股票数据有多种用途,比如说IBM有价格更新,那么我需要在我的投资组合中更新IBM的价值。以及在屏幕上显示新数据。并且可以有其他几种同时使用。updatePosition(tickerId, price) 和 updateStockScreen(tickerId, price)。此外,从位置更新中分离Gui更新非常重要,因为GUI不是应用程序的主要优势。
  4. 我对如何实现这种类型的设计感到困扰。我在QT中阅读了有关模型/视图设计的信息,但是如果View线程从同一个地图中读取,则必须将其锁定。这导致设计缓慢/低效。每次视图从模型中读取时,都需要锁定模型。这是在实时GUI中提供的吗?
  5. 总而言之,我已将许多不同的对象存储为地图。对象实时更新。我需要更新它们,然后在不同的位置使用它们。如果有人能给我一个关于如何实现这种设计的小例子,那就太好了。

对有用书籍的一些参考也受到赞赏。

我是新人,并试图用我的小知识实现太多,所以如果我问过愚蠢/形容不对的问题,请原谅我。

谢谢 希夫


6247
2018-02-28 16:01


起源

我刚才在这里回答了一个类似的问题: stackoverflow.com/questions/9476045/...虽然你的问题写得好得多,所以谢谢你!同意@HostileFork,我觉得使用信号是沟通数据的最佳方式。我想知道,你能在一个线程中运行什么样的视图?这是非gui观吗? - jdi


答案:


这听起来在概念上就像你想要一个线程上的模型和另一个线程上的视图,我在一个点上看到了。

如果是这样......并且您的模型通过视图小部件是只读的,那么是的,您必须锁定。我认为这样做会破坏模型/视图分离所提供的“解耦”的优雅。但它可以成功。

但是......如果您的模型是通过视图进行读写,则无法正确执行 一点都不 因为通知槽的排队性质。这是我在qt-interest邮件列表上关于该主题的邮件列表对话的存档:

http://blog.hostilefork.com/qt-model-view-different-threads/

“简短的版本是我认为模型不可行
  在非GUI线程上进行修改......无论模型是否正确
  数据已通过读/写锁保护。如果我正在收集
  是正确的,那么Qt应该有一个断言模型和
  它的视图具有相同的线程关联(现在似乎没有这样做)“

由KDE开发人员进行的后续单元测试验证了这一点。

我觉得解决这个问题的最好方法是将模型和视图保持在同一个线程上,并且只修改GUI线程中的模型。因此,如果工作者线程希望更改它,那么它应该使用信号。

工作者是否需要保留自己创建模型的数据副本(或者当用户通过视图更改模型时是否需要通知以使其保持最新)取决于您的应用程序。如果我理解正确的话,听起来好像你可能只是通过信号/插槽传送更新而忘记工作人员......


10
2018-02-28 20:56



+1表示通过信号进行通信 - jdi
好吧,模型完全由一个单独的线程更新,我无法控制它。我想我需要提出某种管道设计模型来实现我需要的东西。 - shiv chawla
@ShivChawla需要对来自单独线程的事件做出反应并不意味着你也必须拥有 QAbstractItemModel在该线程中实例化的派生类。模型应该只存在于GUI线程中。获得它有点棘手,但请仔细阅读/重新阅读上面的链接,以了解为什么会这样... - HostileFork
@HostileFork其实你是对的。如果传入的更改可以某种方式转移到模型/视图线程,那么它将起作用。我需要了解更多有关此信号槽如何工作的信息。但考虑到这种情况,工作线程非常迅速地向模型线程发出信号。反过来,每当视图询问模型中的数据时,它可能必须等待,因为信号正在更新模型。这不是类似的情况还是我错误地理解了?信号有什么特别之处? - shiv chawla
@ShivChawla:请务必阅读一篇关于“线程亲和力”的文章 QObject 及其对信号/插槽的影响。您可能希望工作线程立即收集更新块,并限制它向GUI发送批量更新信号的速率。这可能会减少GUI线程在执行工作人员触发的插槽时所花费的开销。从Thinker-Qt检查我的SignalThrottler类可能会有所帮助: gitorious.org/thinker-qt/thinker-qt/blobs/master/src/... - HostileFork


即使模型是只读的,我今天也学到了另一个潜在的问题。我使用另一个线程来修改模型中的数据(实际上,我的程序超过20个线程,它们都很好),然后Qt计时器更新。这很有效,但是我遇到了一个问题,那就是:

你无法锁定 rowCount/columnCount 和 data()

Qt按顺序工作,这意味着在人类语言中,它会问“你有多大”,然后问“你在这个位置有什么数据”,这些都容易打破。

考虑:

int FilesQueue::rowCount(const QModelIndex &/*parent*/) const
{
    std::lock_guard<decltype(mainQueueMutex)> lg(mainQueueMutex);
    return filesQueue.size();
}
QVariant FilesQueue::data(const QModelIndex &index, int role) const
{
    std::lock_guard<decltype(mainQueueMutex)> lg(mainQueueMutex);
    if ( role == Qt::DisplayRole) {
        return filesQueue[index.row()]->getFilename();
    }
}

Qt会做这样的调用:

//...
obj->rowCount();
obj->data(...);
//...

而且我在整个地方都有断言失败,因为简单地介于两者之间 rowCount() 和 data(),有一个线程正在改变数据的大小!它打破了这个计划。所以这发生了:

//...
obj->rowCount();
//another thread: filesQueue.erase(...)
obj->data(...);
//...

我对此问题的解决方案是再次在data()方法中验证大小:

QVariant FilesQueue::data(const QModelIndex &index, int role) const
{
    std::lock_guard<decltype(mainQueueMutex)> lg(mainQueueMutex);
    //solution is here:
    if(static_cast<int>(filesQueue.size()) <= index.row())
        return QVariant();
    if ( role == Qt::DisplayRole) {
        return filesQueue[index.row()]->getFilename();
    }
}

我生命中有3个小时我永远不会回来:-)


1
2018-06-04 21:04