@WTFAYD

Можно ли сделать QJSEngine thread-safe?

Я разрабатываю программу, в которой объект 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 
    // ...  
}
  • Вопрос задан
  • 123 просмотра
Решения вопроса 1
Подскажите пожалуйста, можно ли каким-то способом обезопасить обращения к QJSEngine из других потоков?

Можно через систему сигнал/слот передавать информацию в поток, в котором создан объект QJSEngine и там выполнять QJSEngine ::call. Используйте флаг Qt::QueuedConnection для подключения слотов к нужному вам сигналу.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Войти через центр авторизации
Похожие вопросы