-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
N-API: no ability to schedule work on different thread? (via uv_async_t) #13512
Comments
/cc @nodejs/n-api |
(AFAIK |
Looking at the code now, |
At this time, it is not a goal for N-API to cover all of the functionality offered by We (the N-API working group) discussed extensively how much of What we settled on for now was to just include basic async functionality in N-API, because it can be a little tricky to get right without some higher-level abstractions. The N-API async APIs (and the
Why did you need |
Doesn't |
|
Ok, I guess I'm confused about what's being asked then. I thought @gpean was looking to do work on another thread, which those methods do (in the libuv thread pool). |
I think the issue is that Node also uses the uv default loop as its main event loop. |
I was not very familiar with libuv and the builtin thread pool you mention - I just read http://docs.libuv.org/en/v1.x/threadpool.html. One issue I see is that you cannot control the pool size other than via an environment variable? It also still competes with the JS work and operations, since this is the same loop. But, I agree it covers most of the use cases for async work. There can still be use cases where a native library you want to integrate already has its threading done, or you don't want to use the libuv thread pool, and you might just want to notify JS when stuff is available, using uv_async_t. Not the majority of use cases, but still a valid and broad use case IMO. RE why you need to include v8.h: you won't be able to get a |
Said otherwise, the issue I see is that, |
@gpean is the need to schedule work on a separate thread for an existing module or something new ? |
Adding to @jasongin comments we only included coverage for the use case provided by AsyncWorker as it seemed to be broadly used in existing modules. If there is another pattern that is also broadly used in the existing modules we can take a look. Best way to start would be a list of modules that use the additional pattern. |
@mhdawson it's for new stuff I propose that we focus on the following, non major, actionable issues:
|
You can stash the We had a |
Don't do this! A |
You probably mean the uv_async_t data field. & What about this statement from the doc:
?
I know, I copied pasted the |
Please elaborate on how is this "not so async". Its what all the async operations in node do, so its as async as fs, dns.loopup, etc. Its possible to create your own uv loop, and there are a very, very few occaisons when it is necessary. execSync and friends do it, for example, so they can use uv to do the exec, but they use a different loop so as to prevent any concurrent processing or progress on the main loop. Inspector does it as well, because it needs a way to interact with a remote debugger when the main thread is blocked in the debugger. Its pretty rare, and note that both of them involve wanting to do I/O while causing the default loop to be completely blocked. |
You justly mentioned two use cases where blocking syscalls, or libc or other system library calls, or potentially long computations can impact the main loop thread pool availability and that for this reason, you create a separate loop/pool for them. This is the "not so async" i'm talking about - Having everybody on the same loop means potential resource fighting (and so, implicit synchronization) for an available slot on the (fixed 4 threads wide!) thread pool, which we could make it possible to avoid easily in N-API by just letting people be able to request a new thread pool. |
That is, you cannot call a persisted |
@kkoopa isn't that what |
(But, I get the point, I'm happy to use the initial isolate to perform the call back- Makes things simpler!) So, effectively, one issue remains: that you're forcing N-API users to use 4 threads to perform async work which can be potentially already used by Javascript IO work. When you have a 32 cores server, I'd think you might want to make it scale better- if you don't want to expose loop control, at least the default pool size should match the vcpu count. |
Without looking up the function in question: No, that is not how it works. IIRC, |
@kkoopa how does it work wrt reference tracking though? how do I guarantee that my JS function value won't have been collected, by the time my uv_async callback fires? I still need the reference creation to have things protected from GC no? |
Also. I need to create a EDIT: yeah, visibly I missed |
A void foo(v8::FunctionCallbackInfo<v8::Value> info) {
v8::HandleScope scope;
v8::Local<v8::Function> my_callback = info[0].As<v8::Function>();
schedule_async_stuff(my_callback);
} //<--- All (local) C++ variables, HandleScope dies, along with the local handle, now there might be a risk of garbage collection void foo(v8::FunctionCallbackInfo<v8::Value> info) {
v8::HandleScope scope;
v8::Local<v8::Function> my_callback = info[0].As<v8::Function>();
v8::Persistent<v8::Function> leaky_reference(info.GetIsolate(), my_callback);
schedule_async_stuff(my_callback);
} //<-- There is a persistent reference stored to the function referenced by my_callback, so the function lives. However, the v8::Persistent is destroyed by C++ scoping rules, but its underlying storage cell in V8 is not, now you have no way of clearing up the storage cell |
Yes you must have a scope in libuv callbacks, since they are outside Node (which sets up a scope before going from V8 to a C++ callback in an addon). There is both a |
Thanks. At least I can remove the v8.h dependency. w00t! |
To clear things up a bit further: A |
Can you please clarify the comments you made last June regarding the lack of multi-threading support in NAN and N-API?
The original author of the issue was questioning whether http://docs.libuv.org/en/v1.x/guide/threads.html#id1
My understanding based on the documentation and source is that create_async_work will run a task in a separate thread. It's true the main node.js event loop is used, but that appears to be for the event signalling (i.e. results) not for the actual processing itself. uv_queue_work is defined right inside of threadpool.c.. In the logic for uv_queue_work, line 287, it's placing the work onto the uv__queue_work worker queue and not the event loop. While I may be interpreting this wrong, both the source and official libuv documentation indicate separate threads are used. So why did you say this isn't the case for N-API? |
@ebickle "different thread" in this context means starting a new thread. Nan and n-api use existing threads. |
@bnoordhuis I'm still not seeing that in the source. https://github.com/nodejs/node/blob/master/src/node_api.cc#L2871 http://docs.libuv.org/en/v1.x/threadpool.html
Documentation is unambiguous. uv_queue_work runs the work on one of the threads (multiple threads) in the threadpool and the result is then synchronized back onto the main event loop. https://nodejs.org/en/docs/guides/dont-block-the-event-loop/
We use the Node.js OracleDB driver extensively and tuned the number of worker threads it uses via UV_THREADPOOL_SIZE since we're DB I/O heavy. From their source: Not much different from N-API: I'm not seeing any logic inside of n-api that's forcing a single threaded worker pool? What am I missing? :) |
Okay, let me try again. Read "different thread" as "new standalone thread made with uv_thread_create()". Nan and n-api don't do that, they use the thread pool. |
Bundle a `uv_async_t`, a `uv_idle_t`, a `uv_mutex_t`, a `uv_cond_t`, and a `v8::Persistent<v8::Function>` to make it possible to call into JS from another thread. The API accepts a void data pointer and a callback which will be invoked on the loop thread and which will receive the `napi_value` representing the JavaScript function to call so as to perform the call into JS. The callback is run inside a `node::CallbackScope`. A `std::queue<void*>` is used to store calls from the secondary threads, and an idle loop is started by the `uv_async_t` callback on the loop thread to drain the queue, calling into JS with each item. Items can be added to the queue blockingly or non-blockingly. The thread-safe function can be referenced or unreferenced, with the same semantics as libuv handles. Re: nodejs/help#1035 Re: #20964 Fixes: #13512 PR-URL: #17887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Bundle a `uv_async_t`, a `uv_idle_t`, a `uv_mutex_t`, a `uv_cond_t`, and a `v8::Persistent<v8::Function>` to make it possible to call into JS from another thread. The API accepts a void data pointer and a callback which will be invoked on the loop thread and which will receive the `napi_value` representing the JavaScript function to call so as to perform the call into JS. The callback is run inside a `node::CallbackScope`. A `std::queue<void*>` is used to store calls from the secondary threads, and an idle loop is started by the `uv_async_t` callback on the loop thread to drain the queue, calling into JS with each item. Items can be added to the queue blockingly or non-blockingly. The thread-safe function can be referenced or unreferenced, with the same semantics as libuv handles. Re: nodejs/help#1035 Re: nodejs#20964 Fixes: nodejs#13512 PR-URL: nodejs#17887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Bundle a `uv_async_t`, a `uv_idle_t`, a `uv_mutex_t`, a `uv_cond_t`, and a `v8::Persistent<v8::Function>` to make it possible to call into JS from another thread. The API accepts a void data pointer and a callback which will be invoked on the loop thread and which will receive the `napi_value` representing the JavaScript function to call so as to perform the call into JS. The callback is run inside a `node::CallbackScope`. A `std::queue<void*>` is used to store calls from the secondary threads, and an idle loop is started by the `uv_async_t` callback on the loop thread to drain the queue, calling into JS with each item. Items can be added to the queue blockingly or non-blockingly. The thread-safe function can be referenced or unreferenced, with the same semantics as libuv handles. Re: nodejs/help#1035 Re: #20964 Fixes: #13512 Backport-PR-URL: #25002 PR-URL: #17887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Bundle a `uv_async_t`, a `uv_idle_t`, a `uv_mutex_t`, a `uv_cond_t`, and a `v8::Persistent<v8::Function>` to make it possible to call into JS from another thread. The API accepts a void data pointer and a callback which will be invoked on the loop thread and which will receive the `napi_value` representing the JavaScript function to call so as to perform the call into JS. The callback is run inside a `node::CallbackScope`. A `std::queue<void*>` is used to store calls from the secondary threads, and an idle loop is started by the `uv_async_t` callback on the loop thread to drain the queue, calling into JS with each item. Items can be added to the queue blockingly or non-blockingly. The thread-safe function can be referenced or unreferenced, with the same semantics as libuv handles. Re: nodejs/help#1035 Re: #20964 Fixes: #13512 Backport-PR-URL: #25002 PR-URL: #17887 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
Bundle a `uv_async_t`, a `uv_idle_t`, a `uv_mutex_t`, a `uv_cond_t`, and a `v8::Persistent<v8::Function>` to make it possible to call into JS from another thread. The API accepts a void data pointer and a callback which will be invoked on the loop thread and which will receive the `napi_value` representing the JavaScript function to call so as to perform the call into JS. The callback is run inside a `node::CallbackScope`. A `std::queue<void*>` is used to store calls from the secondary threads, and an idle loop is started by the `uv_async_t` callback on the loop thread to drain the queue, calling into JS with each item. Items can be added to the queue blockingly or non-blockingly. The thread-safe function can be referenced or unreferenced, with the same semantics as libuv handles. Re: nodejs/help#1035 Re: nodejs/node#20964 Fixes: nodejs/node#13512
Correct me if I'm wrong, but I didn't see a way to schedule work on a different thread using N-API, and/or a way to notify the main loop that some work is done from some other thread and that we need to wake up with a callback call (via uv_async_t and a persistent callback).
I think that's a major use case of native modules (doing stuff in // of the JS thread), so it's kind of surprising. What was the rationale during N-API design?
I was able to hack something using v8.h and uv.h, of course. But it kinda defeats the purpose of having a portable API.
The text was updated successfully, but these errors were encountered: