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

15
我正在创建一个应用程序,它显示市场数据并在其他表单中使用它。我将市场数据存储在一个称为 std::map<tickerId, StockData> 的映射中。让我们来看一下该映射如何被使用的一个例子。
1.网络发送一个数据包,封装了时间t的股票数据。updatePrice(tickerId, latestPrice) 2.更新映射中的股票数据。现在,多个线程可以访问/更新数据。因此,必须对映射进行锁定以进行线程安全操作。这里是第一个问题,我需要锁定底层数据进行更新吗?
3.新的股票数据有多种用途,例如,IBM的价格更新,那么我需要在我的组合中更新IBM的价值。同时在屏幕上显示新数据。还有可能存在其他几个同时使用的情况。updatePosition(tickerId, price)updateStockScreen(tickerId, price)。此外,将GUI更新与位置更新分开很重要,因为GUI不是应用程序的主要功能。
4.我只是困扰于如何实现这种类型的设计。我读到QT中的Model / View设计来显示数据,但如果View线程从同一映射中读取,则必须对其进行锁定。这会导致缓慢/低效的设计。每次视图从模型中读取时,都需要锁定模型。在实时GUI中是否首选?
5.总之,我已经将许多不同的对象存储为映射。并且对象正在实时更新。我需要更新它们,然后在各个位置使用它们。如果有人能给我一个小例子来实现这样的设计,那就太好了。
欢迎提供一些有用的书籍参考。
对不起,我是新手,试图用我的渺小的知识实现太多目标,请原谅我问了愚蠢/错误的问题。
谢谢 Shiv

我刚在这里回答了一个类似的问题:https://dev59.com/1mDVa4cB1Zd3GeqPh_Dq#9476153,尽管你的问题写得更好,所以谢谢你!同意@HostileFork的观点,我认为使用信号是传递数据的最佳方式。不过我想知道,在线程中你能运行什么样的视图?是非GUI视图吗? - jdi
2个回答

17

听起来你想在一个线程上放置模型(model)而在另一个线程上放置视图(view),我曾经调查过这一点。

如果是这样的话...并且你的模型通过视图部件是只读的,那么是的,你必须锁定。 我认为这样做会破坏模型/视图分离提供的“解耦”优雅性。 但它可能被改进。

然而...如果您的模型通过视图是可读写的,那么根本不可能正确地完成,因为通知插槽(queued nature of the notification slots)的排队性质。 这是关于该主题的qt-interest邮件列表上的一次通讯记录的存档:

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

 

“简而言之,我认为不管模型的数据是否已受到读/写锁保护,都不应在非GUI线程上修改模型……。 如果我收集的信息正确,则Qt可能应该有一个断言,即模型和其视图具有相同的线程亲和性(现在似乎没有)”

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

我认为解决这个问题的最佳方法是将模型和视图保持在同一个线程上,并仅在GUI线程中修改模型。 因此,如果工作线程希望更改它,则应使用信号(signal)。

是否需要工作线程保留其自己从模型创建的数据的副本(或者如果它需要获取通知以在用户通过视图更改模型时保持它们的最新状态)取决于您的应用程序。 如果我正确理解你的话,看起来你可能只需通过信号/插槽传递更新并忘记它们即可。


模型是由完全独立的线程更新的,我对此没有控制权。我认为我需要提出一种管道设计模型来实现我所需求的功能。 - shiv chawla
1
@ShivChawla 需要对来自另一个线程的事件做出反应并不意味着您必须在该线程中实例化 QAbstractItemModel 派生类。模型应仅存在于 GUI 线程上。这有点棘手,但请仔细阅读/重新阅读上面的链接,以了解为什么会出现这种情况... - HostileFork says dont trust SE
@HostileFork 实际上,你是对的。如果传入的更改可以以某种方式转移到Model/View线程,则它将起作用。我需要了解更多关于这个信号槽如何工作的知识。但考虑到这种情况,工作线程非常快地向模型线程发出信号。反过来,每当视图从模型请求数据时,它可能必须等待,因为模型正在被信号更新。这不是类似的情况吗?还是我理解有误?信号有什么特别之处? - shiv chawla
1
@ShivChawla:一定要了解QObject的“线程亲和性”,以及它对信号/槽的影响。 您可能希望您的工作线程一次收集一块更新,并减缓发送批量更新信号到GUI的速率。 这可能会减少在GUI线程执行由工作者触发的槽时所花费的开销时间。 建议查看我在Thinker-Qt中的SignalThrottler类:http://gitorious.org/thinker-qt/thinker-qt/blobs/master/src/signalthrottler.cpp - HostileFork says dont trust SE
@HostileFork 感谢您的回复。我仔细思考了您的回复,现在我理解了线程亲和性的概念以及模式视图的工作原理。我想我明白了。所以这里有一个后续问题。如果我向模型发出更新信号,那么一切都很好。但是假设我也需要在工作线程中使用该模型,是否需要在工作对象中单独/复制模型呢?想想一个显示只是次要任务的应用程序。 - shiv chawla
@shivchawla 在StackOverflow上的跟进问题应该是新问题,而不是旧问题的评论。 :) 如果您愿意,您可以始终链接到以前的问题... - HostileFork says dont trust SE

2
今天我学到了另一个潜在的问题,通过错误的方式发现了即使模型是只读的。我使用另一个线程来修改模型中的数据(实际上,我的程序有超过20个线程,它们都很好地协作),然后Qt计时器进行更新。这非常有效,但我遇到了一个问题,即: rowCount/columnCountdata()之间不能锁定。 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();
    }
}

我的三个小时就这样白白浪费了 :-)


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