我今天遇到了令人沮丧的问题。我正在使用node-ffi
在我的电子应用程序中运行C ++代码。总的来说,我有很好的经历,但我今天开始使用多线程并遇到了一些困难。我传入的ffi
回调从线程调用就好了。但是当我结束循环并尝试join
循环线程到主线程时,它会完全冻结电子应用程序。
完全免责声明:我对C ++很陌生,并希望对我的代码提出任何反馈意见,以改善它,特别是你认为我应该注意的任何红旗。
以下两个回购展示了我遇到的错误:
电子项目 - 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();
}
这是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();
}
}
然后我的测试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);
主电子过程在app.js中运行。我希望看到
start stream
Frame Ready (3800)
stop stream
end session
但它没有显示end session
。但是,如果我删除c ++中的行frameReadyCB()
,它会按预期工作。所以ffi回调参考不知何故搞砸了多线程环境。很想对此有所了解。谢谢!
答案 0 :(得分:0)
您的申请已陷入僵局。在您的示例中,您有两个线程:
$ npm start
和Threader::start()
。在 thread-2 中,您调用frameReadyCB()
,它将阻止该线程,直到它完成为止。 previous answer显示回调将在 thread-1 上执行。
不幸的是, thread-1 已经忙于第二个setTimeout,正在调用stopStreaming()
。 Threader::stop
尝试加入 thread-2 ,阻塞直至 thread-2 完成。
你现在陷入僵局。 thread-2 正在等待 thread-1 执行回调, thread-1 正在等待 thread-2 完成执行。他们都在互相等待。
当使用async()
通过node-ffi创建线程时,似乎node-ffi处理在单独线程上运行的回调。因此,您可以从C ++库中删除线程,而是从节点库中调用DllLib.startStreaming.async(() => {})
。
为了解决这个问题,您需要确保在等待frameReadyCB()
完成时不要尝试加入 thread-2 。您可以使用互斥锁执行此操作。此外,当 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;
};
// 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();
}
}
// 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;