来自 C++ 线程的节点 FFI 回调

Jak*_*uhy 4 javascript c++ multithreading node-ffi electron

我今天遇到了一个令人沮丧的问题。我正在node-ffi我的电子应用程序中运行 C++ 代码。总的来说,我有很好的经验,但我今天开始使用多线程并遇到了一些困难。我传入的回调ffi是从线程调用的。然而,当我结束循环并尝试将join循环线程连接到主线程时,它完全冻结了电子应用程序。

完整免责声明:我对 C++ 还很陌生,并且希望对我的代码提供任何反馈以改进它,特别是您认为我应该注意的任何危险信号。

以下两个存储库演示了我遇到的错误:
Electron Project - https://github.com/JakeDluhy/threading-test
C++ DLL - https://github.com/JakeDluhy/ThreadedDll

以下是我正在做的事情的概述:
在我的 dll 中,我公开了开始/结束会话和开始/停止流式传输的函数。它们调用类实例的引用来实际实现功能。本质上,它是更强大的 C++ 类的 C 包装器。

// ThreadedDll.h
#pragma once

#ifdef __cplusplus
extern "C" {
#endif

#ifdef THREADEDDLL_EXPORTS
#define THREADEDDLL_API __declspec(dllexport)
#else
#define THREADEDDLL_API __declspec(dllimport)
#endif
    THREADEDDLL_API void beginSession(void(*frameReadyCB)());
    THREADEDDLL_API void endSession();

    THREADEDDLL_API void startStreaming();
    THREADEDDLL_API void stopStreaming();
#ifdef __cplusplus
}
#endif

// ThreadedDll.cpp
#include "ThreadedDll.h"
#include "Threader.h"

static Threader *threader = NULL;

void beginSession(void(*frameReadyCB)())
{
    threader = new Threader(frameReadyCB);
}

void endSession()
{
    delete threader;
    threader = NULL;
}

void startStreaming()
{
    if (threader) threader->start();
}

void stopStreaming()
{
    if (threader) threader->stop();
}
Run Code Online (Sandbox Code Playgroud)

该类如下Threader所示:

// Threader.h
#pragma once

#include <thread>
#include <atomic>

using std::thread;
using std::atomic;

class Threader
{
public:
    Threader(void(*frameReadyCB)());
    ~Threader();

    void start();
    void stop();
private:
    void renderLoop();

    atomic<bool> isThreading;
    void(*frameReadyCB)();
    thread myThread;
};

// Threader.cpp
#include "Threader.h"

Threader::Threader(void(*frameReadyCB)()) :
    isThreading{ false },
    frameReadyCB{ frameReadyCB }
{
}


Threader::~Threader()
{
    if (myThread.joinable()) myThread.join();
}

void Threader::start()
{
    isThreading = true;

    myThread = thread(&Threader::renderLoop, this);
}

void Threader::stop()
{
    isThreading = false;

    if (myThread.joinable()) myThread.join();
}

void Threader::renderLoop()
{
    while (isThreading) {
        frameReadyCB();
    }
}
Run Code Online (Sandbox Code Playgroud)

然后这是我使用它的测试 javascript:

// ThreadedDll.js
const ffi = require('ffi');
const path = require('path');

const DllPath = path.resolve(__dirname, '../dll/ThreadedDll.dll');
// Map the library functions in the way that FFI expects
const DllMap = {
    'beginSession':     [ 'void', [ 'pointer' ] ],
    'endSession':       [ 'void', [] ],

    'startStreaming':   [ 'void', [] ],
    'stopStreaming':    [ 'void', [] ],
};
// Create the Library using ffi, the DLL, and the Function Table
const DllLib = ffi.Library(DllPath, DllMap);

class ThreadedDll {
    constructor(args) {
        this.frameReadyCB = ffi.Callback('void', [], () => {
            console.log('Frame Ready');
        });

        DllLib.beginSession(this.frameReadyCB);
    }

    startStreaming() {
        DllLib.startStreaming();
    }

    stopStreaming() {
        DllLib.stopStreaming();
    }

    endSession() {
        DllLib.endSession();
    }
}

module.exports = ThreadedDll;

// app.js
const ThreadedDll = require('./ThreadedDll');

setTimeout(() => {
    const threaded = new ThreadedDll();
    console.log('start stream');
    threaded.startStreaming();

    setTimeout(() => {
        console.log('stop stream');
        threaded.stopStreaming();
        console.log('end session');
        threaded.endSession();
    }, 1000);
}, 2000);
Run Code Online (Sandbox Code Playgroud)

主要的 Electron 进程在 app.js 中运行。我希望看到

start stream
Frame Ready (3800)
stop stream
end session
Run Code Online (Sandbox Code Playgroud)

但它显示没有end sessionframeReadyCB()但是,如果我删除c++ 中的行,它就会按预期工作。因此, ffi 回调引用不知何故搞砸了多线程环境。很想对此有一些想法。谢谢!

And*_*erk 5

问题

您的申请陷入僵局。在您的示例中,您有两个线程:

  1. thread-1 - 当您运行时创建$ npm start,并且
  2. thread-2 - 在 中创建Threader::start()

thread-2中,您调用frameReadyCB(),这将阻塞线程直到它完成。之前的答案显示回调将在thread-1上执行。

不幸的是,thread-1已经忙于第二个 setTimeout,调用stopStreaming()Threader::stop尝试加入thread-2,阻塞直到thread-2完成。

你现在陷入了僵局。线程2正在等待线程1执行回调,线程1正在等待线程2完成执行。他们俩都在等待对方。

通过node-ffi解决方案

当使用 node-ffi 创建线程时,node-ffi 似乎会处理在单独线程上运行的回调async()。因此,您可以从 C++ 库中删除线程,而是DllLib.startStreaming.async(() => {})从节点库中调用。

通过C++解决方案

为了解决这个问题,您需要确保在线程 2等待frameReadyCB()完成时永远不会尝试加入它。您可以使用互斥锁来完成此操作。另外,您需要确保当thread-2正在等待时,您不会等待锁定互斥体frameReadyCB()。执行此操作的唯一方法是创建另一个线程来停止流式传输。下面的示例使用 node-ffi 执行此操作async,尽管可以在 C++ 库中完成此操作以将其隐藏在节点库中。

// Threader.h
#pragma once

#include <thread>
#include <atomic>

using std::thread;
using std::atomic;
using std::mutex;

class Threader
{
public:
    Threader(void(*frameReadyCB)());
    ~Threader();

    void start();
    void stop();
private:
    void renderLoop();

    atomic<bool> isThreading;
    void(*frameReadyCB)();
    thread myThread;
    mutex mtx;
};
Run Code Online (Sandbox Code Playgroud)
// Threader.cpp
#include "Threader.h"

Threader::Threader(void(*frameReadyCB)()) :
    isThreading{ false },
    frameReadyCB{ frameReadyCB }
{
}


Threader::~Threader()
{
    stop();
}

void Threader::start()
{
    isThreading = true;

    myThread = thread(&Threader::renderLoop, this);
}

void Threader::stop()
{
    isThreading = false;

    mtx.lock();
    if (myThread.joinable()) myThread.join();
    mtx.unlock();
}

void Threader::renderLoop()
{
    while (isThreading) {
        mtx.lock();
        frameReadyCB();
        mtx.unlock();
    }
}
Run Code Online (Sandbox Code Playgroud)
// ThreadedDll.js
const ffi = require('ffi');
const path = require('path');

const DllPath = path.resolve(__dirname, '../dll/ThreadedDll.dll');
// Map the library functions in the way that FFI expects
const DllMap = {
    'beginSession':     [ 'void', [ 'pointer' ] ],
    'endSession':       [ 'void', [] ],

    'startStreaming':   [ 'void', [] ],
    'stopStreaming':    [ 'void', [] ],
};
// Create the Library using ffi, the DLL, and the Function Table
const DllLib = ffi.Library(DllPath, DllMap);

class ThreadedDll {
    constructor(args) {
        this.frameReadyCB = ffi.Callback('void', [], () => {
            console.log('Frame Ready');
        });

        DllLib.beginSession(this.frameReadyCB);
    }

    startStreaming() {
        DllLib.startStreaming();
    }

    stopStreaming() {
        DllLib.stopStreaming.async(() => {});
    }

    endSession() {
        DllLib.endSession.async(() => {});
    }
}

module.exports = ThreadedDll;
Run Code Online (Sandbox Code Playgroud)