[Qt-interest] AmIRight: Can't Have Model and View on different Threads?

Hostile Fork hostilefork at gmail.com
Fri Aug 7 07:36:30 CEST 2009


Hello all...!

I'm on day #2 of looking at Qt's Model/View architecture, and had a  
question that turned into an essay.  By the end of it, I figured I'd  
most likely answered what I started with... but I thought I'd run it  
by qt-interest (for criticisms if I'm wrong, and for Internet- 
Searching-Posterity if I'm right!)

The short version is that I don't think it's feasible for a Model to  
be modified on a non-GUI thread...regardless of whether the model's  
data has been protected with read/write locks.   If what I'm gathering  
is correct, then Qt should probably have an assert that a model and  
its view have the same thread affinity (it doesn't seem to do that now)

Here's what I think doesn't work:

	class NotReallyThreadSafeStandardItemModel :
		 public QStandardItemModel
	{
	private:
		QReadWriteLock dataLock;
	public:
		bool insertRows(int row, int count, const QModelIndex& parent =  
QModelIndex()) {
			dataLock.lockForWrite();
			QStandardItemModel::insertRows(row, count, parent);
			dataLock.unlock();
		}
		QVariant data(const QModelIndex& index, int role = Qt::DisplayRole)  
const {
			dataLock.lockForRead();
			QVariant result = QStandardItemModel::data(index, role);
			dataLock.unlock();
			return result;
		}
		...
	};

What I see as the problem is that the model/view updating strategy  
relies on signals and slots, and it's not expecting them to be queued.

For instance: the Gui View might see a model has 4 items, and set a  
variable ItemsDrawn to 4.  Then later it gets a notification saying  
"I'm adding one row", so it sets ItemsDrawn to 5 and tries to fetch  
the 5th element to draw.  But this could fail if that item has already  
been removed from the data by another thread.  This may have happened,  
but the "I'm deleting one row" notification is still pending in the  
Gui queue after the current "adding" notification we are processing...!

This would seem to suggest that it is not practical to have a model  
and view on separate threads.  As a thought-experiment, I've scribbled  
some rough code which tries for a "correct" 2-threaded  
StandardItemModel:

	class TwoThreadedStandardItemModel : public QStandardItemModel {
	private:
		QReadWriteLock dataLock;
		QMutex insertMutex;
		QWaitCondition insertDone;
		bool insertSuccess;
		...
	signals:
		bool insertRowsSignal(int row, int count, const QModelIndex & parent);
	protected slots:
		void insertRowsSlot(int row, int count, const QModelIndex & parent) {
			assert(currentThreadIsGui());
			assert(sender()->thread() == workerThread());
			dataLock.lockForWrite();
			bool success = QStandardItemModel::insertRows(row, count, parent);
			dataLock.unlock();

			insertMutex.lock();
			insertSuccess = success;
			insertDone.wakeOne();
			insertMutex.unlock();
		}
		...
	public:
		bool insertRow(int row, int count, const QModelIndex & parent =  
QModelIndex()) {
			bool success;
			if (currentThreadIsWorker()) {
				insertMutex.lock();
				emit insertRowsSignal(row, count, parent, false); // cross-thread,  
queued
				insertDone.wait(&insertMutex);
				success = insertSuccess;
				insertMutex.unlock();
			} else {
				dataLock.lockForWrite();
				success = QStandardItemModel::insertRows(row, count, parent);
				dataLock.unlock();
			}
			return success;
		}
		...
		QVariant data(const QModelIndex & index, int role = Qt::DisplayRole)  
const {
			// reading does not need to use a signal
			dataLock.lockForRead();
			QVariant result = QStandardItemModel::data(index, role);
			dataLock.unlock();
			return result;
		}
		...
		ThreadsafeStandardItemModel () {
			connect(this, SIGNAL(insertRowsSignal(int, int, const QModelIndex&,  
bool),
				this, SLOT(insertRowsSlot(int, int, const QModelIndex&, bool));
			...
		}
		...
	};

Despite the synchronization overhead, I imagine this approach could  
avoid crashes.  But if the View class you are using can do any  
autonomous modifications (like drag and drop rearrangement), then it  
could break the expectations of worker thread code, such as:

	model->insertRow(0);
		// Gui could modify model here!
	model->setData(model->index(0, 0), subject);
		// Gui could modify model here!
	model->setData(model->index(0, 1), sender);
		// Gui could modify model here!
	model->setData(model->index(0, 2), date);

Any of those opportunities for modification could delete or move the  
row we are working with.  A better strategy is for the worker to do  
updates via larger atomic asynchronous operations queued to the GUI  
thread.  Each should have no reliance on the prior to have finished to  
perform the next, e.g.

	public slots:
		void addMailMessage(const QString &subject, const QString &sender,  
const QDateTime &date)
		{
			assert(currentThreadIsGui()); // Modifications below will be atomic!
			model->insertRow(0);
			model->setData(model->index(0, 0), subject);
			model->setData(model->index(0, 1), sender);
			model->setData(model->index(0, 2), date);
		}

So... errr... AmiIRight? :)

Thanks!
	Brian

---
http://hostilefork.com
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.qt-project.org/pipermail/qt-interest-old/attachments/20090806/70fbf195/attachment.html 


More information about the Qt-interest-old mailing list