Перейти к основному содержимому

Non-deterministic order of signal propagation

Race conditions in single-threaded QML/QtQuick? It's more likely than you think!

Properties in QML are known to be declarative, and are so convenient that you may be tempting to use them for everything. But let's break down a case where things might go subtly south.

Situation

Suppose you have a source property abc which you have to validate before using it. It may be defined in QML or C++ — it doesn't matter; so let's use QML for simplicity.

property int abc

The validation code could be an arbitrary complex condition, or could even be as simple as reading only the property value itself:

// OK
if (abc && somethingElse) {
use(abc);
}
// Also OK
if (abc) {
use(abc);
}

The validation expression becomes long and repetitive, so at some point you decide to factor it out into a property, and replace it in consumers:

// Bad
readonly property bool check: abc && somethingElse
// or simply
readonly property bool check: abc

// some other property binding or signal handler
consumer: {
if (check) {
use(abc);
}
}

It doesn't matter how complex your check expression is, it would be equally broken at this point. But not always. Sometimes. Maybe? It's non-deterministic, after all!

Welcome to street racing

So what could possibly go wrong with that check intermediary property as a condition?

First, we need to understand how the QML engine does it magic, how it knows when to automatically re-evaluate property bindings.

к сведению

In Qt all properties have some sort of READ function. In C++ READ may simply return a value from a backing storage (there's also a MEMBER syntax for such cases which reduces the boilerplate), or it can be an arbitrary complex method with all sorts of custom logic in it. But properties declared in pure QML (as of Qt 6.8) are all stored, and there's no support for JavaScript get/set yet.

Thus, reading a property declared in pure QML is guaranteed to return its stored value. It will NOT cause the property's binding to re-evaluate.

Second, in Qt properties should either be CONSTANT or have an associated NOTIFY signal — and if they don't, MOC complains at compile time. We are interested in the ones with NOFITY here. Luckily, the QML engine generates associated NOFITY signals for properties declared in pure QML for us.

During a property binding evaluation, whenever the engine READs a property which happens to have a NOTIFY, the engine would connect that signal to a handler that would re-evaluate the property:

consumer: abc ? use(abc) : null
// connect(abcChanged, updateConsumer)

vs.

check: abc
// connect(abcChanged, updateCheck)

consumer: check ? use(abc) : null
// connect(checkChanged, updateConsumer)
// Additionally, if the `check` was truthy, we also READ `abc` in the first branch:
// connect(abcChanged, updateConsumer)

Notice that abcChanged may be connected to update two different properties. But the order in which the signal will be dispatched is not specified. How should the runtime know that we want to handle updates in a particular order?

We want the following to happen:

  1. The abc property changes, so the abcChanged signal is emitted.
  2. The signal propagates to updateCheck first, which in turn emits checkChanged which is handled by updateConsumer. The binding re-evaluates, and reads updated values of both abc and check properties.
  3. Lastly, the abcChanged signal also propagates to updateConsumer (and causes an excessive re-evaluation, by the way).

But suppose the following sequence of events happens:

  1. The abc property changes, so the abcChanged signal is emitted.
  2. The signal propagates to the updateConsumer connection first.
  3. The engine starts re-evaluating the binding for consumer.
    • The engine READs the value of check
    • But check has not been updated yet!
    • Now you are using use(abc) when you were not supposed to!
    • You've been outraced by signals.

How to fix

There are two ways to fix this: refactor the proxy property into a method/function, or inline it back.

к сведению

Methods defined in pure QML and function in JavaScript scripts & modules follow the same rules during property binding evaluation as if their bodies were inlined in the binding itself: the engine would still subscribe to all the NOTIFY signals along the way to update the property that's currently being evaluated.

So don't be afraid to use functions and methods! Especially because they may take input arguments to further factor out more boilerplate. Also, QML methods can be optionally typed.

// Good
function check(): bool {
return abc;
}

consumer: check() ? use(abc) : null

As a bonus point, you are no longer wasting memory on storing a cached value.

But it's a bug in Qt!

No. It's only a bug in the architecture of your application's data flow, specifically in cache invalidation (because such intermediary property is effectively a cache). If you substitute Qt for any SQL database with STORED non-GENERATED columns and triggers, or a distributed service architecture with microservices notifying and querying each other and caching the results — you'd get the exact same picture everywhere.

Let's conduct a thought experiment, and try to replicate this issue in pure C++ without any QML, just to show how obviously incorrect this approach is under the hood without any of the QML syntax sugar.

Imagine that the property abc is in one class, while check is in another and consumer is something entirely external.

readonly property bool check: abc

consumer: {
if (check) {
use(abc);
}
}
class AbcObject : public QObject
{
Q_OBJECT
Q_PROPERTY(int abc READ abc WRITE setAbc NOTIFY abcChanged)
public:
using QObject::QObject;
int abc() const
{
return m_abc;
}
void setAbc(int value)
{
// the usual boilerplate
if (m_abc != value) {
m_abc = value;
Q_EMIT abcChanged();
}
}
Q_SIGNALS:
void abcChanged();
private:
int m_abc = 0;
};

class MyObject : public QObject
{
Q_OBJECT
Q_PROPERTY(bool check READ isCheck NOTIFY checkChanged)
public:
using QObject::QObject;
void init(AbcObject *abcObj);
bool isCheck() const;
Q_SIGNALS:
void checkChanged();
private:
// private setter, not part of the property declaration
void setCheck(bool check);
bool m_check = false;
};

void MyObject::init(AbcObject *abcObj)
{
// this is an equivalent to what QQmlEngine implements to update readonly
// properties via private setters
connect(&abcObj, &AbcObject::abcChanged, this, [&]() {
setCheck(abcObj.abc() != 0 && somethingElse);
});
// for demonstration purposes, let's not bother with disconnecting from a
// previous AbcObject instance, if any.
}

bool MyObject::isCheck() const
{
// serving value from the private member
return m_check;
}

void MyObject::setCheck(bool check)
{
// the usual boilerplate
if (m_check != check) {
m_check = check;
Q_EMIT checkChanged();
}
}

void main()
{
AbcObject abcObj;
MyObject myObj;
myObj.init(&abcObj);
auto updateConsumer = [&]() {
if (myObj.check()) {
use(abcObj.abc());
}
};
// Let's pretend the lifetimes are OK here
connect(&abcObj, &AbcObject::abcChanged, updateConsumer);
connect(&myObj, &MyObject::checkChanged, updateConsumer);

abcObj.setAbc(42);
}

Here, two things are connected to the abcChanged() signal, and there are no guarantees as to which one is going to be dispatched to first. Different language, slightly more complicated implementation than anyone would think of writing, same problem.

It could be fixed by not storing a cached duplicate of the value, and querying the real source directly instead. Of course, this would require MyObject to keep a handle of AbcObject (that is, if the properties belong to different classes like in this example).

function check(): bool {
return abc;
}

consumer: {
if (check()) {
use(abc);
}
}
class MyObject : public QObject
{
// ...
private:
QPointer<AbcObject> m_abcObj;
}
void MyObject::init(AbcObject *abcObj)
{
if (m_abcObj) {
disconnect(m_abcObj, &AbcObject::abcChanged, this, &MyObject::checkChanged);
}
m_abcObj = abcObj;
if (m_abcObj) {
connect(m_abcObj, &AbcObject::abcChanged, this, &MyObject::checkChanged);
}
Q_EMIT checkChanged();
}

bool MyObject::isCheck() const
{
// serving value disrectly from the source object
return m_abcObj ? m_abcObj->abc() : false;
}

// remove setCheck(), it is not needed

Now, no matter which signal propagates to the consumer first, the getter will always fetch the real up-to-date data.

tl;dr

It can not be stressed enough:

подсказка

Don't use one property as a safeguard to another! Do not factor out expressions as readonly proxy properties! Do use functions!

And that's it. Stay safe, and don't get speeding tickets for cutting corners.

Resources

Qt documentation: