如何从 webworker 中取消 wasm 进程
How to cancel a wasm process from within a webworker
我有一个 wasm 进程(从 C++ 编译而来)处理 Web 应用程序内部的数据。假设必要的代码如下所示:
std::vector<JSONObject> data
for (size_t i = 0; i < data.size(); i++)
{
process_data(data[i]);
if (i % 1000 == 0) {
bool is_cancelled = check_if_cancelled();
if (is_cancelled) {
break;
}
}
}
这段代码基本上"runs/processes a query"类似于一个SQL查询界面:
但是,查询可能需要几分钟才能完成 run/process,并且用户可以随时取消他们的查询。取消过程将发生在正常的 javascript/web 应用程序中,在 service Worker 之外 运行ning wasm。
然后我的问题是什么是我们如何知道用户单击了 'cancel' 按钮并将其传达给 wasm 进程以便知道该进程已被取消以便它可以退出的示例?使用 worker.terminate()
不是一个选项,因为我们需要为 worker
保留所有加载的数据并且不能仅仅杀死那个工人(它需要保持其存储数据的活力,因此可以进行另一个查询运行...).
在 javascript 和 worker/wasm/c++ 应用程序之间进行通信的示例方法是什么,以便我们可以知道何时退出以及如何正确退出?
此外,让我们假设一个典型的查询需要 60 秒到 运行 并使用 cpp/wasm.
在浏览器中处理 500MB 的数据
更新: 我认为这里有以下可能的解决方案,基于一些研究(以及下面的初始 answers/comments)和一些反馈:
使用两个 worker,一个 worker 存储数据,另一个 worker 处理数据。这样 processing-worker 就可以终止,而数据会一直保留。 可行吗? 不可行,因为每次启动时将 ~ 500MB 的数据复制到 webworker 会花费太多时间。这可以(以前)使用 SharedArrayBuffer 完成,但由于一些安全问题,现在它的支持相当 limited/nonexistent。太糟糕了,因为如果支持的话,这似乎是迄今为止最好的解决方案...
使用单个工人使用 Emterpreter 并使用 emscripten_sleep_with_yield
。 可行吗?不,使用 Emterpreter(在上面的文档中提到)时会破坏性能,并使所有查询速度降低大约 4-6 倍。
总是 运行 第二个工作人员,在 UI 中只显示最近的工作人员。 可行吗?不,如果它不是共享数据结构并且数据大小为 500MB x 2 = 1GB(500MB 似乎是一个在现代桌面 运行 中 browser/computer).
使用 API 调用服务器来存储状态并检查查询是否已取消。 可行吗? 是的,尽管从每个 运行ning 查询中每秒对网络请求进行长轮询似乎很费力。
使用增量解析方法,一次只解析一行。 可行吗? 是的,但也需要大量重写解析函数,以便每个函数都支持它(实际的数据解析是在几个函数中处理的——过滤、搜索、计算、分组、排序等等等等
使用 IndexedDB 并将状态存储在 javascript 中。在 WASM 中分配一块内存,然后 return 它的指针指向 JavaScript。然后在那里读取数据库并填充指针。然后用 C++ 处理你的数据。 可行吗? 不确定,但如果可以实施,这似乎是最佳解决方案。
[还有什么吗?]
在赏金中,我想知道三件事:
- 以上六点分析是否普遍有效?
- 我还缺少其他(也许更好)的方法吗?
- 任何人都可以展示一个非常基本的示例来执行 #6 - 如果可能并且跨浏览器工作,这似乎是最好的解决方案。
对于Chrome(仅)您可以使用共享内存(共享缓冲区作为内存)。当你想停止时,在内存中升起一个标志。不太喜欢这个解决方案(很复杂并且仅在 chrome 中受支持)。这也取决于你的查询是如何工作的,以及是否有冗长查询可以检查标志的地方。
相反,您可能应该多次调用 c++ 函数(例如,针对每个查询)并检查是否应该在每次调用后停止(只需向工作人员发送消息以停止)。
我所说的多次是指分阶段进行查询(单个查询的多个函数调用)。它可能不适用于您的情况。
无论如何,AFAIK 无法向 Webassembly 执行发送信号(例如 Linux kill)。因此,您必须等待操作完成才能完成取消。
我附上了一个可以解释这个想法的代码片段。
worker.js:
... init webassembly
onmessage = function(q) {
// query received from main thread.
const result = ... call webassembly(q);
postMessage(result);
}
main.js:
const worker = new Worker("worker.js");
const cancel = false;
const processing = false;
worker.onmessage(function(r) {
// when worker has finished processing the query.
// r is the results of the processing.
processing = false;
if (cancel === true) {
// processing is done, but result is not required.
// instead of showing the results, update that the query was canceled.
cancel = false;
... update UI "cancled".
return;
}
... update UI "results r".
}
function onCancel() {
// Occurs when user clicks on the cancel button.
if (cancel) {
// sanity test - prevent this in UI.
throw "already cancelling";
}
cancel = true;
... update UI "canceling".
}
function onQuery(q) {
if (processing === true) {
// sanity test - prevent this in UI.
throw "already processing";
}
processing = true;
// Send the query to the worker.
// When the worker receives the message it will process the query via webassembly.
worker.postMessage(q);
}
一个用户体验角度的想法:
你可以创建~两个工人。这将占用两倍的内存,但将允许您 "cancel" "immediately" 一次。 (这只是意味着在后端,第二个工作人员将 运行 下一个查询,当第一个工作人员完成取消时,取消将再次立即生效。
共享线程
由于 worker 和它调用的 C++ 函数共享同一个线程,因此 worker 也会被阻塞,直到 C++ 循环完成,并且无法处理任何传入的消息。我认为 a solid 选项将通过从主应用程序一次初始化一个迭代来最小化线程被阻塞的时间量。
看起来像这样。
main.js -> worker.js -> C++ function -> worker.js -> main.js
打破循环
下面,C++ 有一个初始化为 0 的变量,它会在每次循环迭代时递增并存储在内存中。
C++ 函数然后执行循环的一次迭代,递增变量以跟踪循环位置,然后立即中断。
int x;
x = 0; // initialized counter at 0
std::vector<JSONObject> data
for (size_t i = x; i < data.size(); i++)
{
process_data(data[i]);
x++ // increment counter
break; // stop function until told to iterate again starting at x
}
然后您应该能够 post 向 Web Worker 发送一条消息,然后 Web Worker 向 main.js 发送一条消息,表明该线程不再被阻塞。
正在取消操作
从这一点,main.js知道web worker线程不再被阻塞,可以决定是否告诉web worker再次执行C++函数(C++变量跟踪内存中的循环增量。)
let continueOperation = true
// here you can set to false at any time since the thread is not blocked here
worker.expensiveThreadBlockingFunction()
// results in one iteration of the loop being iterated until message is received below
worker.onmessage = function(e) {
if (continueOperation) {
worker.expensiveThreadBlockingFunction()
// execute worker function again, ultimately continuing the increment in C++
} {
return false
// or send message to worker to reset C++ counter to prepare for next execution
}
}
继续行动
假设一切顺利,并且用户没有取消操作,循环应该继续直到完成。请记住,您还应该针对循环是否已完成或需要继续发送一条不同的消息,这样您就不会一直阻塞工作线程。
我有一个 wasm 进程(从 C++ 编译而来)处理 Web 应用程序内部的数据。假设必要的代码如下所示:
std::vector<JSONObject> data
for (size_t i = 0; i < data.size(); i++)
{
process_data(data[i]);
if (i % 1000 == 0) {
bool is_cancelled = check_if_cancelled();
if (is_cancelled) {
break;
}
}
}
这段代码基本上"runs/processes a query"类似于一个SQL查询界面:
但是,查询可能需要几分钟才能完成 run/process,并且用户可以随时取消他们的查询。取消过程将发生在正常的 javascript/web 应用程序中,在 service Worker 之外 运行ning wasm。
然后我的问题是什么是我们如何知道用户单击了 'cancel' 按钮并将其传达给 wasm 进程以便知道该进程已被取消以便它可以退出的示例?使用 worker.terminate()
不是一个选项,因为我们需要为 worker
保留所有加载的数据并且不能仅仅杀死那个工人(它需要保持其存储数据的活力,因此可以进行另一个查询运行...).
在 javascript 和 worker/wasm/c++ 应用程序之间进行通信的示例方法是什么,以便我们可以知道何时退出以及如何正确退出?
此外,让我们假设一个典型的查询需要 60 秒到 运行 并使用 cpp/wasm.
在浏览器中处理 500MB 的数据更新: 我认为这里有以下可能的解决方案,基于一些研究(以及下面的初始 answers/comments)和一些反馈:
使用两个 worker,一个 worker 存储数据,另一个 worker 处理数据。这样 processing-worker 就可以终止,而数据会一直保留。 可行吗? 不可行,因为每次启动时将 ~ 500MB 的数据复制到 webworker 会花费太多时间。这可以(以前)使用 SharedArrayBuffer 完成,但由于一些安全问题,现在它的支持相当 limited/nonexistent。太糟糕了,因为如果支持的话,这似乎是迄今为止最好的解决方案...
使用单个工人使用 Emterpreter 并使用
emscripten_sleep_with_yield
。 可行吗?不,使用 Emterpreter(在上面的文档中提到)时会破坏性能,并使所有查询速度降低大约 4-6 倍。总是 运行 第二个工作人员,在 UI 中只显示最近的工作人员。 可行吗?不,如果它不是共享数据结构并且数据大小为 500MB x 2 = 1GB(500MB 似乎是一个在现代桌面 运行 中 browser/computer).
使用 API 调用服务器来存储状态并检查查询是否已取消。 可行吗? 是的,尽管从每个 运行ning 查询中每秒对网络请求进行长轮询似乎很费力。
使用增量解析方法,一次只解析一行。 可行吗? 是的,但也需要大量重写解析函数,以便每个函数都支持它(实际的数据解析是在几个函数中处理的——过滤、搜索、计算、分组、排序等等等等
使用 IndexedDB 并将状态存储在 javascript 中。在 WASM 中分配一块内存,然后 return 它的指针指向 JavaScript。然后在那里读取数据库并填充指针。然后用 C++ 处理你的数据。 可行吗? 不确定,但如果可以实施,这似乎是最佳解决方案。
[还有什么吗?]
在赏金中,我想知道三件事:
- 以上六点分析是否普遍有效?
- 我还缺少其他(也许更好)的方法吗?
- 任何人都可以展示一个非常基本的示例来执行 #6 - 如果可能并且跨浏览器工作,这似乎是最好的解决方案。
对于Chrome(仅)您可以使用共享内存(共享缓冲区作为内存)。当你想停止时,在内存中升起一个标志。不太喜欢这个解决方案(很复杂并且仅在 chrome 中受支持)。这也取决于你的查询是如何工作的,以及是否有冗长查询可以检查标志的地方。
相反,您可能应该多次调用 c++ 函数(例如,针对每个查询)并检查是否应该在每次调用后停止(只需向工作人员发送消息以停止)。
我所说的多次是指分阶段进行查询(单个查询的多个函数调用)。它可能不适用于您的情况。
无论如何,AFAIK 无法向 Webassembly 执行发送信号(例如 Linux kill)。因此,您必须等待操作完成才能完成取消。
我附上了一个可以解释这个想法的代码片段。
worker.js:
... init webassembly
onmessage = function(q) {
// query received from main thread.
const result = ... call webassembly(q);
postMessage(result);
}
main.js:
const worker = new Worker("worker.js");
const cancel = false;
const processing = false;
worker.onmessage(function(r) {
// when worker has finished processing the query.
// r is the results of the processing.
processing = false;
if (cancel === true) {
// processing is done, but result is not required.
// instead of showing the results, update that the query was canceled.
cancel = false;
... update UI "cancled".
return;
}
... update UI "results r".
}
function onCancel() {
// Occurs when user clicks on the cancel button.
if (cancel) {
// sanity test - prevent this in UI.
throw "already cancelling";
}
cancel = true;
... update UI "canceling".
}
function onQuery(q) {
if (processing === true) {
// sanity test - prevent this in UI.
throw "already processing";
}
processing = true;
// Send the query to the worker.
// When the worker receives the message it will process the query via webassembly.
worker.postMessage(q);
}
一个用户体验角度的想法: 你可以创建~两个工人。这将占用两倍的内存,但将允许您 "cancel" "immediately" 一次。 (这只是意味着在后端,第二个工作人员将 运行 下一个查询,当第一个工作人员完成取消时,取消将再次立即生效。
共享线程
由于 worker 和它调用的 C++ 函数共享同一个线程,因此 worker 也会被阻塞,直到 C++ 循环完成,并且无法处理任何传入的消息。我认为 a solid 选项将通过从主应用程序一次初始化一个迭代来最小化线程被阻塞的时间量。
看起来像这样。
main.js -> worker.js -> C++ function -> worker.js -> main.js
打破循环
下面,C++ 有一个初始化为 0 的变量,它会在每次循环迭代时递增并存储在内存中。 C++ 函数然后执行循环的一次迭代,递增变量以跟踪循环位置,然后立即中断。
int x;
x = 0; // initialized counter at 0
std::vector<JSONObject> data
for (size_t i = x; i < data.size(); i++)
{
process_data(data[i]);
x++ // increment counter
break; // stop function until told to iterate again starting at x
}
然后您应该能够 post 向 Web Worker 发送一条消息,然后 Web Worker 向 main.js 发送一条消息,表明该线程不再被阻塞。
正在取消操作
从这一点,main.js知道web worker线程不再被阻塞,可以决定是否告诉web worker再次执行C++函数(C++变量跟踪内存中的循环增量。)
let continueOperation = true
// here you can set to false at any time since the thread is not blocked here
worker.expensiveThreadBlockingFunction()
// results in one iteration of the loop being iterated until message is received below
worker.onmessage = function(e) {
if (continueOperation) {
worker.expensiveThreadBlockingFunction()
// execute worker function again, ultimately continuing the increment in C++
} {
return false
// or send message to worker to reset C++ counter to prepare for next execution
}
}
继续行动
假设一切顺利,并且用户没有取消操作,循环应该继续直到完成。请记住,您还应该针对循环是否已完成或需要继续发送一条不同的消息,这样您就不会一直阻塞工作线程。