Я разрабатываю программу, в которой объект
QJSEngine и принадлежащие ему объекты
QJSValue используются в нескольких потоках.
Основная проблема заключается в том, что программа крашится в рандомный момент, если в другом потоке (не тот поток, в котором был создан
QJSEngine) производить какие-то операции с объектами
QJSValue: вызов
QJSValue::call(), использование
QJSValueIterator и т.д.
Крашится программа в реализации
QJSEngine, в основном в функциях, где происходит выделение\удаление памяти. Например:
// QV4::PersistentValueStorage::allocate() (qv4persistent.cpp):
Value *PersistentValueStorage::allocate()
{
Page *p = static_cast<Page *>(firstPage);
while (p) {
if (p->header.freeList != -1)
break;
p = p->header.next;
}
if (!p)
p = allocatePage(this);
Value *v = p->values + p->header.freeList;
p->header.freeList = v->int_32(); // !!! Здесь крашится
// ....
}
В интернетах наткнулся на багрепорт с похожей проблемой. Там это объясняют тем, что
QJSEngine не thread-safe, поэтому он и его
QJSValue должны использоваться в том же потоке, в каком и были созданы.
Подскажите пожалуйста, можно ли каким-то способом обезопасить обращения к
QJSEngine из других потоков? Это критически важно для моей программы.
Тестовый пример из багрепорта, в котором ловится этот баг:
#include <QCoreApplication>
#include <QJSEngine>
#include <QThread>
#include <QTimer>
int main(int argc, char** argv)
{
QCoreApplication app(argc, argv);
QJSEngine engine;
engine.installExtensions(QJSEngine::ConsoleExtension);
engine.evaluate("var iteration = 0;");
auto function = engine.evaluate(R"((
function()
{
if (++iteration % 100 == 0)
console.log(iteration);
}
))");
QThread thread;
thread.start();
QTimer timer;
QObject::connect(&timer, &QTimer::timeout, &engine, [&]{function.call();}, Qt::DirectConnection);
timer.moveToThread(&thread); // Comment it if you want to test QJSEngine in the main thread
QMetaObject::invokeMethod(&timer, "start", Q_ARG(int, 0));
return app.exec();
}
P.S. Я попытался обернуть вызовы
QJSValue::call() и методов
QJSEngine в
QMetaObject::invokeMethod(), чтобы вызовы этих функций производились в том потоке, в котором создан
QJSEngine. В теории это должно было решить проблему, но на практике ни на что не повлияло.
threadsafeqjsengine.h
class ThreadSafeQJSEngine : public QObject
{
Q_OBJECT
QJSEngine* m_engine;
public:
ThreadSafeQJSEngine(QObject *parent = Q_NULLPTR);
virtual ~ThreadSafeQJSEngine();
// ...
QJSValue call(QJSValue value, const QJSValueList& args);
// ...
private slots:
inline QJSValue call_imp(QJSValue value, const QJSValueList& args) {
return value.engine() == m_engine ? value.call(args) : QJSValue();
}
// ...
};
threadsafeqjsengine.cpp
ThreadSafeQJSEngine::ThreadSafeQJSEngine(QObject *parent)
: QObject(parent)
{
m_engine = new QJSEngine;
}
ThreadSafeQJSEngine::~ThreadSafeQJSEngine()
{
delete m_engine;
}
QJSValue ThreadSafeQJSEngine::call(QJSValue value, const QJSValueList &args)
{
if (QThread::currentThread() != this->thread())
{
QJSValue result;
QMetaObject::invokeMethod(this,
"call_imp",
Qt::BlockingQueuedConnection,
Q_RETURN_ARG(QJSValue, result),
Q_ARG(QJSValue, value),
Q_ARG(const QJSValueList&, args)
);
return result;
}
else
{
return call_imp(value, args);
}
}
// ...
main.cpp
int main(int argc, char** argv)
{
QCoreApplication app(argc, argv);
ThreadSafeQJSEngine engine;
// The same as before
// ...
QObject::connect(&timer, &QTimer::timeout, &engine, [&]{engine.call(function);}, Qt::DirectConnection);
// The same as before
// ...
}