首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >在多线程程序中使用QJSEngine崩溃

在多线程程序中使用QJSEngine崩溃
EN

Stack Overflow用户
提问于 2022-10-19 06:40:36
回答 2查看 62关注 0票数 0

我有一个程序,其中一个QJSEngine对象与几个不同的线程一起使用。不应该存在并发访问--创建一个线程,调用或计算某个线程,然后删除该线程。

然而,在使用QJSEngine时,程序会随机崩溃。所有崩溃都发生在与分配或释放内存相关的私有QJSEngine函数中。示例:

代码语言:javascript
复制
// 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();   // !!! Get SEGFAULT here

    // ....
}

我找到了一个与我的问题类似的布格利波特。记者提供了再现问题的最低限度代码:

代码语言:javascript
复制
#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();
}

在主线程(创建QJSEngine的线程)中执行相同的操作不会使程序崩溃。

您能告诉我如何在这种情况下使QJSEngine线程安全吗?报告提供的模板函数safeEngineCall()可以将引擎调用封装在阻塞队列中,但我不知道如何使用它。

提前谢谢。

UPD:我有一个想法,将QJSValue::call()封装到一个线程安全函数中,通过使用QMetaObject::invokeMethod()强制它在QJSEngine的对象线程中调用。

threadsafeqjsengine.h

代码语言:javascript
复制
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

代码语言:javascript
复制
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

代码语言:javascript
复制
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 
    // ...  
}

但是问题并没有消失,它像往常一样崩溃了。我做错了什么?

EN

回答 2

Stack Overflow用户

发布于 2022-11-18 12:27:31

我做了一些测试,行为似乎因Qt版本而异。例如,在QT5.12.12 Win10 MSVC中,我甚至无法让您的原始示例崩溃,但它在5.14.2上却很容易崩溃。

这个例子有点做作.如果您想要使JS代码可重用,那么我可能会将其作为模块导入JSE全局对象,并从该模块调用一个函数。模块是缓存的,所以在第一次使用之后,重新导入它(获取引用)并从模块调用函数几乎是“免费”的。那么根本就没有理由保留对JSValue函数的引用。

无论如何,不要问我为什么要这样做,但是在JSEngine::collectGarbage()之后添加一个function.call()似乎可以修复您的示例。或者将计时器间隔减慢到1ms。我怀疑JSValue函数对象存在一些争用条件,在function.call()成为最终修复程序(而不是GC per sé)之后,让引擎“自己收集”。

我还尝试添加互斥(除其他外),但这似乎没有帮助(虽然如果要从多个线程调用引擎,这可能是个好主意)。我还扩展了这个例子,以限制循环的数量,然后(尝试)干净地退出。

代码语言:javascript
复制
#include <QCoreApplication>
#include <QJSEngine>
#include <QMutex>
#include <QThread>
#include <QTimer>
#include <QDebug>

int main(int argc, char** argv)
{
    QCoreApplication app(argc, argv);
    QJSEngine engine;
    engine.installExtensions(QJSEngine::ConsoleExtension);

    engine.evaluate("var iteration = 0;");
    static QJSValue function = engine.evaluate(
        "(function() { if ((++iteration) % 100 == 0) console.log(iteration); })"
    );
    if (function.isError()) {
        qDebug() << function.toString();
        return -1;
    }

    const int interval = 0;  // how often; Try >= 1ms and comment the collectGarbage() call
    const int maxIterations = 20000;  // how many times
    int iteration = 0;  // run counter

    QMutex mutex;
    QTimer timer;
    QObject::connect(&timer, &QTimer::timeout, &engine, [&]
    {
        // QMutexLocker locker(&mutex);  // optional?
        if (!teration) {
             qDebug() << "Calling function from thread" << QThread::currentThread()
                      << "; App thread is" << qApp->thread();
        }
        if (function.isCallable()) {
          function.call();
          engine.collectGarbage();  // < seems to fix things
        }
        else {
          qDebug() << "function is not callable!";
          qApp->exit(1);
        }
        if (iteration == maxIterations) {
          qApp->exit(0);
        }
    }, Qt::DirectConnection);

    if (true)  // false to test QJSEngine in the main thread.
    {
      QThread *thread = new QThread;
      // ensure graceful shutdown
      QObject::connect(qApp, &QCoreApplication::aboutToQuit, qApp, [&]() {
        thread->quit();
        thread->wait();
        thread->deleteLater();
      });
      QObject::connect(thread, &QThread::finished, &timer, &QTimer::stop);
      // move timer and start thread
      timer.moveToThread(thread);
      thread->start();
    }
    else {
      QObject::connect(qApp, &QCoreApplication::aboutToQuit, &timer, &QTimer::stop);
    }

    QMetaObject::invokeMethod(&timer, "start", Q_ARG(int, interval));
    return app.exec();
}

HTH,

-Max

票数 1
EN

Stack Overflow用户

发布于 2022-10-28 06:38:47

我已经找到了解决办法,它基于bugreport创建者的解决方案。为了我的目的,我提供了一个改编而成并提供了一个例子。

如果QJSEngineQJSValue方法是通过带有阻塞队列的QMetaObject::invokeMethod在lambda中调用的,那么它可以正常工作,并且不会发生崩溃。

但是另一方面,如果JS脚本评估需要大量时间,它会阻塞所有其他线程,包括GUI。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/74120887

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档