Skip to content
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

tsfn: implement TypedThreadSafeFunction #742

Merged
merged 46 commits into from
Dec 7, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
78196e0
tsfn: implement ThreadSafeFunctionEx<ContextType>
KevinEady Mar 22, 2020
90ebd80
fix unused parameter errors
KevinEady Mar 22, 2020
fc339c4
wip
KevinEady Mar 31, 2020
fd530b2
wip
KevinEady Apr 7, 2020
561b968
make CallJS template parameter
KevinEady Apr 7, 2020
630d88b
statically check CallJs; add no-Finalizer overload
KevinEady Apr 8, 2020
9ff352c
add tsfn test with tsfn cb function call
KevinEady Apr 17, 2020
6aeecd4
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-api
KevinEady Apr 17, 2020
2da3023
test: Initial commit of TSFNEx threadsafe test
KevinEady Jun 8, 2020
91e8859
test: Modify TSFNEx threadsafe to use TSFNEx
KevinEady Jun 8, 2020
692fbe1
src,test: Add SIGTRAP to TSFNEx::CallJS
KevinEady Jun 8, 2020
0d26703
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Jun 8, 2020
89181da
test: fix TSFNEx tests
KevinEady Jun 12, 2020
8fcdb4d
test: add barebones new tsfn test for use in docs
KevinEady Jun 12, 2020
cc8de12
implement optional function callback
KevinEady Jun 13, 2020
6706f96
clean up optional callback implementation
KevinEady Jun 13, 2020
89d2dea
test: wip with tsfnex tests
KevinEady Jun 14, 2020
d2bcc03
test: napi v4,v5 all tests pass
KevinEady Jun 14, 2020
6b7a7d0
test: consolidate duplicated code
KevinEady Jun 14, 2020
7467a3f
test: basic example, standardize identifier names
KevinEady Jun 14, 2020
a01c3c8
test: refactor the the 'empty' tsfnex test
KevinEady Jun 14, 2020
b0e7817
basic multi-threading
KevinEady Jun 14, 2020
44adeea
test: wip with example
KevinEady Jun 14, 2020
c20685b
test: wip with example test
KevinEady Jun 14, 2020
3b24e74
src: consolidate duplicated tsfnex code
KevinEady Jun 15, 2020
a7a5352
test: v4,v5+ tests pass
KevinEady Jun 15, 2020
d092a32
doc: wip with tsfn documentation
KevinEady Jun 17, 2020
75dd422
doc,test: finish TSFNEx
KevinEady Jun 21, 2020
717b602
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Jul 13, 2020
0d84cf7
Test with longer timeout
KevinEady Jul 27, 2020
c8eea6b
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Jul 27, 2020
807fb27
Apply suggestions from code review
KevinEady Aug 9, 2020
f3aa955
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Aug 9, 2020
fd6a2b4
Additional changes from review
KevinEady Aug 9, 2020
ad9333e
test: tsfnex uses ported tsfn tests
KevinEady Aug 18, 2020
e223955
doc: tsfnex example uses ported tsfn example
KevinEady Aug 18, 2020
3405b2b
src,doc: final cleanup
KevinEady Aug 18, 2020
151a914
Apply documentation suggestions from code review
KevinEady Oct 9, 2020
59f27da
Fix common.gypi
KevinEady Oct 10, 2020
b54f5eb
Additional changes from review
KevinEady Oct 10, 2020
7ec9741
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Oct 29, 2020
7a13f86
doc: fix additional typo
KevinEady Oct 29, 2020
4abe7cf
test: rename tsfnex test files
KevinEady Nov 6, 2020
c24c455
Rename to TypedThreadSafeFunction
KevinEady Nov 24, 2020
559ad8c
Merge remote-tracking branch 'upstream/master' into contexted-tsfn-ap…
KevinEady Nov 24, 2020
5e5b9ce
Apply formatting changes
KevinEady Nov 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,9 @@ The following is the documentation for node-addon-api.
- [AsyncWorker](doc/async_worker.md)
- [AsyncContext](doc/async_context.md)
- [AsyncWorker Variants](doc/async_worker_variants.md)
- [Thread-safe Functions](doc/threadsafe_function.md)
- [Thread-safe Functions](doc/threadsafe.md)
- [ThreadSafeFunction](doc/threadsafe_function.md)
- [TypedThreadSafeFunction](doc/typed_threadsafe_function.md)
- [Promises](doc/promises.md)
- [Version management](doc/version_management.md)

Expand Down
124 changes: 124 additions & 0 deletions doc/threadsafe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# Thread-safe Functions

JavaScript functions can normally only be called from a native addon's main
thread. If an addon creates additional threads, then node-addon-api functions
that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be
called from those threads.

When an addon has additional threads and JavaScript functions need to be invoked
based on the processing completed by those threads, those threads must
communicate with the addon's main thread so that the main thread can invoke the
JavaScript function on their behalf. The thread-safe function APIs provide an
easy way to do this. These APIs provide two types --
[`Napi::ThreadSafeFunction`](threadsafe_function.md) and
[`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md) -- as well as
APIs to create, destroy, and call objects of this type. The differences between
the two are subtle and are [highlighted below](#implementation-differences).
Regardless of which type you choose, the APIs between the two are similar.

`Napi::[Typed]ThreadSafeFunction::New()` creates a persistent reference that
holds a JavaScript function which can be called from multiple threads. The calls
happen asynchronously. This means that values with which the JavaScript callback
is to be called will be placed in a queue, and, for each value in the queue, a
call will eventually be made to the JavaScript function.

`Napi::[Typed]ThreadSafeFunction` objects are destroyed when every thread which
uses the object has called `Release()` or has received a return status of
`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`.
The queue is emptied before the `Napi::[Typed]ThreadSafeFunction` is destroyed.
It is important that `Release()` be the last API call made in conjunction with a
given `Napi::[Typed]ThreadSafeFunction`, because after the call completes, there
is no guarantee that the `Napi::[Typed]ThreadSafeFunction` is still allocated.
For the same reason it is also important that no more use be made of a
thread-safe function after receiving a return value of `napi_closing` in
response to a call to `BlockingCall()` or `NonBlockingCall()`. Data associated
with the `Napi::[Typed]ThreadSafeFunction` can be freed in its `Finalizer`
callback which was passed to `[Typed]ThreadSafeFunction::New()`.

Once the number of threads making use of a `Napi::[Typed]ThreadSafeFunction`
reaches zero, no further threads can start making use of it by calling
`Acquire()`. In fact, all subsequent API calls associated with it, except
`Release()`, will return an error value of `napi_closing`.

## Implementation Differences

The choice between `Napi::ThreadSafeFunction` and
`Napi::TypedThreadSafeFunction` depends largely on how you plan to execute your
native C++ code (the "callback") on the Node.js thread.

### [`Napi::ThreadSafeFunction`](threadsafe_function.md)

This API is designed without N-API 5 native support for [the optional JavaScript
function callback feature](https://github.com/nodejs/node/commit/53297e66cb).
`::New` methods that do not have a `Function` parameter will construct a
_new_, no-op `Function` on the environment to pass to the underlying N-API
call.

This API has some dynamic functionality, in that:
- The `[Non]BlockingCall()` methods provide a `Napi::Function` parameter as the
callback to run when processing the data item on the main thread -- the
`CallJs` callback. Since the callback is a parameter, it can be changed for
every call.
- Different C++ data types may be passed with each call of `[Non]BlockingCall()`
to match the specific data type as specified in the `CallJs` callback.

Note that this functionality comes with some **additional overhead** and
situational **memory leaks**:
- The API acts as a "broker" between the underlying `napi_threadsafe_function`,
and dynamically constructs a wrapper for your callback on the heap for every
call to `[Non]BlockingCall()`.
- In acting in this "broker" fashion, the API will call the underlying "make
call" N-API method on this packaged item. If the API has determined the
thread-safe function is no longer accessible (eg. all threads have released
yet there are still items on the queue), **the callback passed to
[Non]BlockingCall will not execute**. This means it is impossible to perform
clean-up for calls that never execute their `CallJs` callback. **This may lead
to memory leaks** if you are dynamically allocating memory.
- The `CallJs` does not receive the thread-safe function's context as a
parameter. In order for the callback to access the context, it must have a
reference to either (1) the context directly, or (2) the thread-safe function
to call `GetContext()`. Furthermore, the `GetContext()` method is not
_type-safe_, as the method returns an object that can be "any-casted", instead
of having a static type.

### [`Napi::TypedThreadSafeFunction`](typed_threadsafe_function.md)

The `TypedThreadSafeFunction` class is a new implementation to address the
drawbacks listed above. The API is designed with N-API 5's support of an
optional function callback. The API will correctly allow developers to pass
`std::nullptr` instead of a `const Function&` for the callback function
specified in `::New`. It also provides helper APIs to _target_ N-API 4 and
construct a no-op `Function` **or** to target N-API 5 and "construct" a
`std::nullptr` callback. This allows a single codebase to use the same APIs,
with just a switch of the `NAPI_VERSION` compile-time constant.

The removal of the dynamic call functionality has the following implications:
- The API does _not_ act as a "broker" compared to the
`Napi::ThreadSafeFunction`. Once Node.js finalizes the thread-safe function,
the `CallJs` callback will execute with an empty `Napi::Env` for any remaining
items on the queue. This provides the ability to handle any necessary cleanup
of the item's data.
- The callback _does_ receive the context as a parameter, so a call to
`GetContext()` is _not_ necessary. This context type is specified as the
**first template argument** specified to `::New`, ensuring type safety.
- The `New()` constructor accepts the `CallJs` callback as the **second type
argument**. The callback must be statically defined for the API to access it.
This affords the ability to statically pass the context as the correct type
across all methods.
- Only one C++ data type may be specified to every call to `[Non]BlockingCall()`
-- the **third template argument** specified to `::New`. Any "dynamic call
data" must be implemented by the user.


### Usage Suggestions

In summary, it may be best to use `Napi::TypedThreadSafeFunction` if:

- static, compile-time support for targeting N-API 4 or 5+ with an optional
JavaScript callback feature is desired;
- the callback can have `static` storage class and will not change across calls
to `[Non]BlockingCall()`;
- cleanup of items' data is required (eg. deleting dynamically-allocated data
that is created at the caller level).

Otherwise, `Napi::ThreadSafeFunction` may be a better choice.
58 changes: 14 additions & 44 deletions doc/threadsafe_function.md
Original file line number Diff line number Diff line change
@@ -1,41 +1,10 @@
# ThreadSafeFunction

JavaScript functions can normally only be called from a native addon's main
thread. If an addon creates additional threads, then node-addon-api functions
that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be
called from those threads.

When an addon has additional threads and JavaScript functions need to be invoked
based on the processing completed by those threads, those threads must
communicate with the addon's main thread so that the main thread can invoke the
JavaScript function on their behalf. The thread-safe function APIs provide an
easy way to do this.

These APIs provide the type `Napi::ThreadSafeFunction` as well as APIs to
create, destroy, and call objects of this type.
`Napi::ThreadSafeFunction::New()` creates a persistent reference that holds a
JavaScript function which can be called from multiple threads. The calls happen
asynchronously. This means that values with which the JavaScript callback is to
be called will be placed in a queue, and, for each value in the queue, a call
will eventually be made to the JavaScript function.

`Napi::ThreadSafeFunction` objects are destroyed when every thread which uses
the object has called `Release()` or has received a return status of
`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`.
The queue is emptied before the `Napi::ThreadSafeFunction` is destroyed. It is
important that `Release()` be the last API call made in conjunction with a given
`Napi::ThreadSafeFunction`, because after the call completes, there is no
guarantee that the `Napi::ThreadSafeFunction` is still allocated. For the same
reason it is also important that no more use be made of a thread-safe function
after receiving a return value of `napi_closing` in response to a call to
`BlockingCall()` or `NonBlockingCall()`. Data associated with the
`Napi::ThreadSafeFunction` can be freed in its `Finalizer` callback which was
passed to `ThreadSafeFunction::New()`.

Once the number of threads making use of a `Napi::ThreadSafeFunction` reaches
zero, no further threads can start making use of it by calling `Acquire()`. In
fact, all subsequent API calls associated with it, except `Release()`, will
return an error value of `napi_closing`.
The `Napi::ThreadSafeFunction` type provides APIs for threads to communicate
with the addon's main thread to invoke JavaScript functions on their behalf.
Documentation can be found for an [overview of the API](threadsafe.md), as well
as [differences between the two thread-safe function
APIs](threadsafe.md#implementation-differences).

## Methods

Expand Down Expand Up @@ -92,7 +61,8 @@ New(napi_env env,
- `maxQueueSize`: Maximum size of the queue. `0` for no limit.
- `initialThreadCount`: The initial number of threads, including the main
thread, which will be making use of this function.
- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`.
- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. It
can be retreived by calling `GetContext()`.
- `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunction`
is being destroyed. This callback will be invoked on the main thread when the
thread-safe function is about to be destroyed. It receives the context and the
Expand All @@ -101,24 +71,24 @@ New(napi_env env,
`uv_thread_join()`. It is important that, aside from the main loop thread,
there be no threads left using the thread-safe function after the finalize
callback completes. Must implement `void operator()(Env env, DataType* data,
Context* hint)`, skipping `data` or `hint` if they are not provided.
Can be retrieved via `GetContext()`.
ContextType* hint)`, skipping `data` or `hint` if they are not provided. Can
be retrieved via `GetContext()`.
- `[optional] data`: Data to be passed to `finalizeCallback`.

Returns a non-empty `Napi::ThreadSafeFunction` instance.

### Acquire

Add a thread to this thread-safe function object, indicating that a new thread
will start making use of the thread-safe function.
will start making use of the thread-safe function.

```cpp
napi_status Napi::ThreadSafeFunction::Acquire()
```

Returns one of:
- `napi_ok`: The thread has successfully acquired the thread-safe function
for its use.
for its use.
- `napi_closing`: The thread-safe function has been marked as closing via a
previous call to `Abort()`.

Expand All @@ -136,7 +106,7 @@ napi_status Napi::ThreadSafeFunction::Release()
Returns one of:
- `napi_ok`: The thread-safe function has been successfully released.
- `napi_invalid_arg`: The thread-safe function's thread-count is zero.
- `napi_generic_failure`: A generic error occurred when attemping to release
- `napi_generic_failure`: A generic error occurred when attempting to release
the thread-safe function.

### Abort
Expand Down Expand Up @@ -258,10 +228,10 @@ Value Start( const CallbackInfo& info )
// Create a native thread
nativeThread = std::thread( [count] {
auto callback = []( Napi::Env env, Function jsCallback, int* value ) {
// Transform native data into JS data, passing it to the provided
// Transform native data into JS data, passing it to the provided
// `jsCallback` -- the TSFN's JavaScript function.
jsCallback.Call( {Number::New( env, *value )} );

// We're finished with the data.
delete value;
};
Expand Down
Loading