nodejs / node

Node.js JavaScript runtime ✨🐢🚀✨
https://nodejs.org
Other
107.6k stars 29.59k forks source link

N-API: no ability to schedule work on different thread? (via uv_async_t) #13512

Closed gpean closed 6 years ago

gpean commented 7 years ago

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.

cjihrig commented 6 years ago

The current status is this:

I don't dislike the idea of N-API smoothing over any incompatibilities. I'd prefer that over restricting what can be changed in libuv because of Node.

mhdawson commented 6 years ago

My thoughts are that N-API should smooth over the essential functionality from uv. Hopefully the 80/20 rule applies so that a small number of additions to N-API covers 80% of usage that might otherwise have to use uv directly.

mhdawson commented 6 years ago

Possibly related https://github.com/nodejs/node/issues/15604

addaleax commented 6 years ago

Fwiw, I’ve written out my suggested approach for solving this inside of libuv directly, rather than adding a ton of functions to N-API, in https://github.com/libuv/libuv/pull/1657.

mika-fischer commented 6 years ago

@addaleax:

So, essentially the point here is: Is the libuv API/ABI going to be stable enough to support addons using them for basic async stuff forever, or can we make it that? Does that sound about right to you?

Not exactly. My reasoning is as follows:

  1. Napi should provide an ABI that is stable across Node.js versions
  2. Calling back from a foreign thread is essential functionality and should be part of that stable ABI
  3. We don't want to dictate ABI stability for libuv -> We need to wrap uv_async_t in napi

If libuv is ok with cementing their libuv API and ABI for this purpose, that's also a solution, although I would still find it more elegant to include this functionality in napi, because then it covers more use-cases. Ideally I would want to write modules only using napi without even having to know that libuv exists...

addaleax commented 6 years ago

If libuv is ok with cementing their libuv API and ABI for this purpose, that's also a solution, although I would still find it more elegant to include this functionality in napi, because then it covers more use-cases.

Fwiw, my PR at https://github.com/libuv/libuv/pull/1657 has been merged, which moves ABI stability largely out of the picture; what remains is API stability, and I think that is something that libuv wants to provide, yes.

I’m not saying N-API can’t provide wrappers here; I have no issue with that but it seems like an layer of indirection that can be avoided.

mika-fischer commented 6 years ago

@addaleax Having these accessor functions is good in any case.

However, an advantage of wrapping this in napi is that via node-addon-api it would be possible to achieve binary compatibility with older Node.js releases like 6.x. If users need to use the accessor functions directly, then binary compatibility is impossible since they just don't exist in older versions...

So all in all I would be in favor of wrapping uv_async_t in napi.

addaleax commented 6 years ago

@mika-fischer Again, this is not an argument against wrapping these in N-API; but:

We’re in that situation already anyway – adding new functions to N-API which don’t work on older versions or don’t work on older versions yet.

And our docs outline have a strategy for that: Performing a version check, and if it passes, load symbols from the current process. I’ve already implemented that for one of my N-API modules, and while it’s kind of cumbersome to do, this could probably be factored out into its own module.

langxiong commented 6 years ago

@addaleax This setimmediate-napi worked as I expected. Thanks.

Globik commented 6 years ago

Why one call of async function from js part runs successfully, but anather one right after brings to the segmentation fault?? I'm using node.js v9.0 n-api. And when I call the second async function via settimeout then theris no segmentation fault? https://github.com/Globik/libqrencode-js/blob/master/simple_async/simple_async.c

ebickle commented 6 years ago

@jasongin

Can you please clarify the comments you made last June regarding the lack of multi-threading support in NAN and N-API?

The NAN APIs also do not allow scheduling work on a different thread. We are assuming that the functionality offered by NAN is good enough for most native addons.

The original author of the issue was questioning whether napi_create_async_work, and therefore uv_queue_work scheduled work on a separate thread. The impression given is that the answer is no, but that seems to contradict the official libuv documentation:

http://docs.libuv.org/en/v1.x/guide/threads.html#id1

uv_queue_work() is a convenience function that allows an application to run a task in a separate thread.

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?

bnoordhuis commented 6 years ago

@ebickle "different thread" in this context means starting a new thread. Nan and n-api use existing threads.

ebickle commented 6 years ago

@bnoordhuis I'm still not seeing that in the source.

https://github.com/nodejs/node/blob/master/src/node_api.cc#L2871 CALL_UV(env, uv_queue_work(event_loop, w->Request(), uvimpl::Work::ExecuteCallback, uvimpl::Work::CompleteCallback));

http://docs.libuv.org/en/v1.x/threadpool.html

When a particular function makes use of the threadpool (i.e. when using uv_queue_work()) libuv preallocates and initializes the maximum number of threads allowed by UV_THREADPOOL_SIZE.

int uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb) Initializes a work request which will run the given work_cb in a thread from the threadpool. Once work_cb is completed, after_work_cb will be called on the loop thread.

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/

Node's Worker Pool is implemented in libuv (docs), which exposes a general task submission API. Node uses the Worker Pool to handle "expensive" tasks. This includes I/O for which an operating system does not provide a non-blocking version, as well as particularly CPU-intensive tasks.

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: if (uv_queue_work(uv_default_loop(), &req, AsyncWorkCallback, AsyncAfterWorkCallback)) {

Not much different from N-API: CALL_UV(env, uv_queue_work(event_loop, w->Request(), uvimpl::Work::ExecuteCallback, uvimpl::Work::CompleteCallback));

I'm not seeing any logic inside of n-api that's forcing a single threaded worker pool? What am I missing? :)

bnoordhuis commented 6 years ago

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.