QAbstractItemModel 线程安全性

4
我的QAbstractItemModel的实现监听一些事件,并在单独的线程中处理更新。 更新处理可能导致模型中的布局和/或数据发生更改。 数据本身的存储由boost::mutex保护,每个对QAbstractItemModel接口函数的调用(如果我理解正确,则在GUI线程中执行),以及更新处理函数(在单独的线程中)都会锁定互斥量。 在锁定与数据()/rowCount()/whatever尝试同时获取的相同互斥量时,是否可以发出信号layoutChanged/dataChanged?
以下是一段代码:
class MyItemModel : public QAbstractItemModel {
    Q_OBJECT
public:

    void processUpdate(const Update& update) {
        Mservice.post([this, update]() {
            boost::lock_guard<boost::mutex> lock (Mlock);
            bool willModifyLayout = checkWillModifyLayout(update)
            bool willModifyData = checkWillModifyData(update);
            if (willModifyLayout) {
                emit layoutAboutToBeChanged();
            }
                Mdata.processUpdate(update);
            if (willModifyLayout) {
                emit layoutChanged();
            }
            else if (willModifyData) {
                emit dataChanged();
            }            
        });
    }

    virtual QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE {
        boost::lock_guard<boost::mutex> lock (Mlock);
        if (index.isValid()) return Mdata.data(index, role);
    }

private:
    boost::mutex Mmutex;
    boost::asio::service Mservice;
    boost::asio::thread MserviceThread;
    DataStorage Mdata;

}

我认为在模型中存储受互斥保护的数据并不是一个好主意。即使缺乏并发,视图经常调用模型的"data()"方法,锁定和解锁互斥量的开销可能足够大。 - Dmitry
如果你的数据足够庞大,你可以将其管理封装在某个 QObject 中,通过信号向实际模型通信数据更改,这些模型将存储数据的轻量级“视图” - 例如,如果你的数据存储长文本,则模型可以包含每个项目的前140个字符,以便在视图中显示。对于访问完整数据(而不是与模型连接的视图,以便不经常发生访问),你可以创建自己的模型API,同步检索数据。 - Dmitry
这确实是正确的方法。但我使用的框架强制要求使用boost的asio::io_service来处理更新。 另一方面,如果QAbstractItemModel接口函数不抛出异常,即使原始代码可能也能工作。如果processUpdate()在GUI线程处于data()调用中等待锁定互斥量时发出layoutChanged(),则不会发生任何错误。 data()将返回一些错误数据(或者可能是空的QVariant()),将在几个时刻内显示它,然后它将处理信号,并很快开始显示正确的数据。 - thedimitrius
Qt提供了QReadWriteLock。例如,您仍然可以使用互斥锁来保护您的模型。当您需要读取模型(例如在data()中)时,请锁定互斥锁以进行读取访问。当您需要更新模型时,请锁定互斥锁以进行写入访问(这将排除其他线程同时进行读取或写入访问)。使用互斥锁的开销可能是一个问题...但这并不是这个应用程序独有的。对代码进行分析以查看是否存在问题。如果没有问题,请保持简单并使用互斥锁。 - Matthew M.
2个回答

2

我找到了答案: 如果模型属于不同的QThread,则该模型的信号将使用Qt::QueuedConnection连接到视图,这是可以的。 但是,如果(默认情况下)模型属于GUI QThread(也称为QCoreApplication::instance()->thread()),则模型的槽将立即执行,导致对data()、columnCount()等的调用,因此这是不好的。


2
它本质上并不是“好的”,请参见http://blog.hostilefork.com/qt-model-view-different-threads/。 - Martin Hennings
有没有一种方法可以强制模型的信号被连接为Qt::QueuedConnection,即使该模型是在与GUI相同的线程上创建的?(该模型创建一个工作线程来调用模型信号) - pooya13

1

QAbstractItemModel 不是线程安全的

这主要是因为跨线程的信号会被排队处理。

想象一下包含QList<int> list;(忽略QModelIndex)的模型的以下场景:

background thread       GUI thread        +         signal queue, abbreviations for readability
[MODEL]                 [VIEW]                      ( )
beginInsertRows(0, 1);   ...                        ( rowsAboutToBeAdded(0, 1) = add(1) )
list << item();          (doing                     ( add(1) )
endInsertRows();        something                   ( add(1), rowsAdded(0, 1) = added(1) )
beginRemoveRows(0, 1);    else)                     ( add(1), added(1), rowsAboutToBeRemoved(0, 1) = rem(1) )
list.removeAt(0);        ...                        ( add(1), added(1), rem(1) )
endRemoveRows(0, 1);     ...                        ( add(1), added(1), rem(1), rowsRemoved(0, 1) = rmvd(1) )
                         rowsAboutToBeAdded(0, 1);  ( added(1), rem(1), rmvd(1) )
                         rowsAdded(0, 1);           ( rem(1), rmvd(1) )
                         possible crash!

原因:

在rowsAdded()方法中,视图最终会调用

model()->data(model()->index(0, 0));

这个模型索引无效,因为模型没有行了。
在最好的情况下,它只会返回一个无效的 QVariant()
在最坏的情况下(没有防御性检查),模型尝试访问 list[0]

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