|
| 1 | +# ThreadSafeFunction |
| 2 | + |
| 3 | +JavaScript functions can normally only be called from a native addon's main |
| 4 | +thread. If an addon creates additional threads, then node-addon-api functions |
| 5 | +that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be |
| 6 | +called from those threads. |
| 7 | + |
| 8 | +When an addon has additional threads and JavaScript functions need to be invoked |
| 9 | +based on the processing completed by those threads, those threads must |
| 10 | +communicate with the addon's main thread so that the main thread can invoke the |
| 11 | +JavaScript function on their behalf. The thread-safe function APIs provide an |
| 12 | +easy way to do this. |
| 13 | + |
| 14 | +These APIs provide the type `Napi::ThreadSafeFunction` as well as APIs to |
| 15 | +create, destroy, and call objects of this type. |
| 16 | +`Napi::ThreadSafeFunction::New()` creates a persistent reference that holds a |
| 17 | +JavaScript function which can be called from multiple threads. The calls happen |
| 18 | +asynchronously. This means that values with which the JavaScript callback is to |
| 19 | +be called will be placed in a queue, and, for each value in the queue, a call |
| 20 | +will eventually be made to the JavaScript function. |
| 21 | + |
| 22 | +`Napi::ThreadSafeFunction` objects are destroyed when every thread which uses |
| 23 | +the object has called `Release()` or has received a return status of |
| 24 | +`napi_closing` in response to a call to `BlockingCall()` or `NonBlockingCall()`. |
| 25 | +The queue is emptied before the `Napi::ThreadSafeFunction` is destroyed. It is |
| 26 | +important that `Release()` be the last API call made in conjunction with a given |
| 27 | +`Napi::ThreadSafeFunction`, because after the call completes, there is no |
| 28 | +guarantee that the `Napi::ThreadSafeFunction` is still allocated. For the same |
| 29 | +reason it is also important that no more use be made of a thread-safe function |
| 30 | +after receiving a return value of `napi_closing` in response to a call to |
| 31 | +`BlockingCall()` or `NonBlockingCall()`. Data associated with the |
| 32 | +`Napi::ThreadSafeFunction` can be freed in its `Finalizer` callback which was |
| 33 | +passed to `ThreadSafeFunction::New()`. |
| 34 | + |
| 35 | +Once the number of threads making use of a `Napi::ThreadSafeFunction` reaches |
| 36 | +zero, no further threads can start making use of it by calling `Acquire()`. In |
| 37 | +fact, all subsequent API calls associated with it, except `Release()`, will |
| 38 | +return an error value of `napi_closing`. |
| 39 | + |
| 40 | +## Methods |
| 41 | + |
| 42 | +### Constructor |
| 43 | + |
| 44 | +Creates a new empty instance of `Napi::ThreadSafeFunction`. |
| 45 | + |
| 46 | +```cpp |
| 47 | +Napi::Function::ThreadSafeFunction(); |
| 48 | +``` |
| 49 | + |
| 50 | +### Constructor |
| 51 | + |
| 52 | +Creates a new instance of the `Napi::ThreadSafeFunction` object. |
| 53 | + |
| 54 | +```cpp |
| 55 | +Napi::ThreadSafeFunction::ThreadSafeFunction(napi_threadsafe_function tsfn); |
| 56 | +``` |
| 57 | +
|
| 58 | +- `tsfn`: The `napi_threadsafe_function` which is a handle for an existing |
| 59 | + thread-safe function. |
| 60 | +
|
| 61 | +Returns a non-empty `Napi::ThreadSafeFunction` instance. |
| 62 | +
|
| 63 | +### New |
| 64 | +
|
| 65 | +Creates a new instance of the `Napi::ThreadSafeFunction` object. The `New` |
| 66 | +function has several overloads for the various optional parameters: skip the |
| 67 | +optional parameter for that specific overload. |
| 68 | +
|
| 69 | +```cpp |
| 70 | +New(napi_env env, |
| 71 | + const Function& callback, |
| 72 | + const Object& resource, |
| 73 | + ResourceString resourceName, |
| 74 | + size_t maxQueueSize, |
| 75 | + size_t initialThreadCount, |
| 76 | + ContextType* context, |
| 77 | + Finalizer finalizeCallback, |
| 78 | + FinalizerDataType* data); |
| 79 | +``` |
| 80 | + |
| 81 | +- `env`: The `napi_env` environment in which to construct the |
| 82 | + `Napi::ThreadSafeFunction` object. |
| 83 | +- `callback`: The `Function` to call from another thread. |
| 84 | +- `[optional] resource`: An object associated with the async work that will be |
| 85 | + passed to possible async_hooks init hooks. |
| 86 | +- `resourceName`: A JavaScript string to provide an identifier for the kind of |
| 87 | + resource that is being provided for diagnostic information exposed by the |
| 88 | + async_hooks API. |
| 89 | +- `maxQueueSize`: Maximum size of the queue. `0` for no limit. |
| 90 | +- `initialThreadCount`: The initial number of threads, including the main |
| 91 | + thread, which will be making use of this function. |
| 92 | +- `[optional] context`: Data to attach to the resulting `ThreadSafeFunction`. |
| 93 | +- `[optional] finalizeCallback`: Function to call when the `ThreadSafeFunction` |
| 94 | + is being destroyed. This callback will be invoked on the main thread when the |
| 95 | + thread-safe function is about to be destroyed. It receives the context and the |
| 96 | + finalize data given during construction (if given), and provides an |
| 97 | + opportunity for cleaning up after the threads e.g. by calling |
| 98 | + `uv_thread_join()`. It is important that, aside from the main loop thread, |
| 99 | + there be no threads left using the thread-safe function after the finalize |
| 100 | + callback completes. Must implement `void operator()(Env env, DataType* data, |
| 101 | + Context* hint)`, skipping `data` or `hint` if they are not provided. |
| 102 | + Can be retreived via `GetContext()`. |
| 103 | +- `[optional] data`: Data to be passed to `finalizeCallback`. |
| 104 | + |
| 105 | +Returns a non-empty `Napi::ThreadSafeFunction` instance. |
| 106 | + |
| 107 | +### Acquire |
| 108 | + |
| 109 | +Add a thread to this thread-safe function object, indicating that a new thread |
| 110 | +will start making use of the thread-safe function. |
| 111 | + |
| 112 | +```cpp |
| 113 | +napi_status Napi::ThreadSafeFunction::Acquire() |
| 114 | +``` |
| 115 | + |
| 116 | +Returns one of: |
| 117 | +- `napi_ok`: The thread has successfully acquired the thread-safe function |
| 118 | +for its use. |
| 119 | +- `napi_closing`: The thread-safe function has been marked as closing via a |
| 120 | +previous call to `Abort()`. |
| 121 | + |
| 122 | +### Release |
| 123 | + |
| 124 | +Indicate that an existing thread will stop making use of the thread-safe |
| 125 | +function. A thread should call this API when it stops making use of this |
| 126 | +thread-safe function. Using any thread-safe APIs after having called this API |
| 127 | +has undefined results in the current thread, as it may have been destroyed. |
| 128 | + |
| 129 | +```cpp |
| 130 | +napi_status Napi::ThreadSafeFunction::Release() |
| 131 | +``` |
| 132 | + |
| 133 | +Returns one of: |
| 134 | +- `napi_ok`: The thread-safe function has been successfully released. |
| 135 | +- `napi_invalid_arg`: The thread-safe function's thread-count is zero. |
| 136 | +- `napi_generic_failure`: A generic error occurred when attemping to release |
| 137 | +the thread-safe function. |
| 138 | + |
| 139 | +### Abort |
| 140 | + |
| 141 | +"Abort" the thread-safe function. This will cause all subsequent APIs associated |
| 142 | +with the thread-safe function except `Release()` to return `napi_closing` even |
| 143 | +before its reference count reaches zero. In particular, `BlockingCall` and |
| 144 | +`NonBlockingCall()` will return `napi_closing`, thus informing the threads that |
| 145 | +it is no longer possible to make asynchronous calls to the thread-safe function. |
| 146 | +This can be used as a criterion for terminating the thread. Upon receiving a |
| 147 | +return value of `napi_closing` from a thread-safe function call a thread must |
| 148 | +make no further use of the thread-safe function because it is no longer |
| 149 | +guaranteed to be allocated. |
| 150 | + |
| 151 | +```cpp |
| 152 | +napi_status Napi::ThreadSafeFunction::Abort() |
| 153 | +``` |
| 154 | + |
| 155 | +Returns one of: |
| 156 | +- `napi_ok`: The thread-safe function has been successfully aborted. |
| 157 | +- `napi_invalid_arg`: The thread-safe function's thread-count is zero. |
| 158 | +- `napi_generic_failure`: A generic error occurred when attemping to abort |
| 159 | +the thread-safe function. |
| 160 | + |
| 161 | +### BlockingCall / NonBlockingCall |
| 162 | + |
| 163 | +Calls the Javascript function in either a blocking or non-blocking fashion. |
| 164 | +- `BlockingCall()`: the API blocks until space becomes available in the queue. |
| 165 | + Will never block if the thread-safe function was created with a maximum queue |
| 166 | + size of `0`. |
| 167 | +- `NonBlockingCall()`: will return `napi_queue_full` if the queue was full, |
| 168 | + preventing data from being successfully added to the queue. |
| 169 | + |
| 170 | +There are several overloaded implementations of `BlockingCall()` and |
| 171 | +`NonBlockingCall()` for use with optional parameters: skip the optional |
| 172 | +parameter for that specific overload. |
| 173 | + |
| 174 | +```cpp |
| 175 | +napi_status Napi::ThreadSafeFunction::BlockingCall(DataType* data, Callback callback) const |
| 176 | + |
| 177 | +napi_status Napi::ThreadSafeFunction::NonBlockingCall(DataType* data, Callback callback) const |
| 178 | +``` |
| 179 | +
|
| 180 | +- `[optional] data`: Data to pass to `callback`. |
| 181 | +- `[optional] callback`: C++ function that is invoked on the main thread. The |
| 182 | + callback receives the `ThreadSafeFunction`'s JavaScript callback function to |
| 183 | + call as an `Napi::Function` in its parameters and the `DataType*` data pointer |
| 184 | + (if provided). Must implement `void operator()(Napi::Env env, Function |
| 185 | + jsCallback, DataType* data)`, skipping `data` if not provided. It is not |
| 186 | + necessary to call into JavaScript via `MakeCallback()` because N-API runs |
| 187 | + `callback` in a context appropriate for callbacks. |
| 188 | +
|
| 189 | +Returns one of: |
| 190 | +- `napi_ok`: The call was successfully added to the queue. |
| 191 | +- `napi_queue_full`: The queue was full when trying to call in a non-blocking |
| 192 | + method. |
| 193 | +- `napi_closing`: The thread-safe function is aborted and cannot accept more |
| 194 | + calls. |
| 195 | +- `napi_invalid_arg`: The thread-safe function is closed. |
| 196 | +- `napi_generic_failure`: A generic error occurred when attemping to add to the |
| 197 | + queue. |
| 198 | +
|
| 199 | +## Example |
| 200 | +
|
| 201 | +```cpp |
| 202 | +#include <chrono> |
| 203 | +#include <thread> |
| 204 | +#include <napi.h> |
| 205 | +
|
| 206 | +using namespace Napi; |
| 207 | +
|
| 208 | +std::thread nativeThread; |
| 209 | +ThreadSafeFunction tsfn; |
| 210 | +
|
| 211 | +Value Start( const CallbackInfo& info ) |
| 212 | +{ |
| 213 | + Napi::Env env = info.Env(); |
| 214 | +
|
| 215 | + if ( info.Length() < 2 ) |
| 216 | + { |
| 217 | + throw TypeError::New( env, "Expected two arguments" ); |
| 218 | + } |
| 219 | + else if ( !info[0].IsFunction() ) |
| 220 | + { |
| 221 | + throw TypeError::New( env, "Expected first arg to be function" ); |
| 222 | + } |
| 223 | + else if ( !info[1].IsNumber() ) |
| 224 | + { |
| 225 | + throw TypeError::New( env, "Expected second arg to be number" ); |
| 226 | + } |
| 227 | +
|
| 228 | + int count = info[1].As<Number>().Int32Value(); |
| 229 | +
|
| 230 | + // Create a ThreadSafeFunction |
| 231 | + tsfn = ThreadSafeFunction::New( |
| 232 | + env, |
| 233 | + info[0].As<Function>(), // JavaScript function called asynchronously |
| 234 | + "Resource Name", // Name |
| 235 | + 0, // Unlimited queue |
| 236 | + 1, // Only one thread will use this initially |
| 237 | + []( Napi::Env ) { // Finalizer used to clean threads up |
| 238 | + nativeThread.join(); |
| 239 | + } ); |
| 240 | +
|
| 241 | + // Create a native thread |
| 242 | + nativeThread = std::thread( [count] { |
| 243 | + auto callback = []( Napi::Env env, Function jsCallback, int* value ) { |
| 244 | + // Transform native data into JS data, passing it to the provided |
| 245 | + // `jsCallback` -- the TSFN's JavaScript function. |
| 246 | + jsCallback.Call( {Number::New( env, *value )} ); |
| 247 | + |
| 248 | + // We're finished with the data. |
| 249 | + delete value; |
| 250 | + }; |
| 251 | +
|
| 252 | + for ( int i = 0; i < count; i++ ) |
| 253 | + { |
| 254 | + // Create new data |
| 255 | + int* value = new int( clock() ); |
| 256 | +
|
| 257 | + // Perform a blocking call |
| 258 | + napi_status status = tsfn.BlockingCall( value, callback ); |
| 259 | + if ( status != napi_ok ) |
| 260 | + { |
| 261 | + // Handle error |
| 262 | + break; |
| 263 | + } |
| 264 | +
|
| 265 | + std::this_thread::sleep_for( std::chrono::seconds( 1 ) ); |
| 266 | + } |
| 267 | +
|
| 268 | + // Release the thread-safe function |
| 269 | + tsfn.Release(); |
| 270 | + } ); |
| 271 | +
|
| 272 | + return Boolean::New(env, true); |
| 273 | +} |
| 274 | +
|
| 275 | +Napi::Object Init( Napi::Env env, Object exports ) |
| 276 | +{ |
| 277 | + exports.Set( "start", Function::New( env, Start ) ); |
| 278 | + return exports; |
| 279 | +} |
| 280 | +
|
| 281 | +NODE_API_MODULE( clock, Init ) |
| 282 | +``` |
| 283 | + |
| 284 | +The above code can be used from JavaScript as follows: |
| 285 | + |
| 286 | +```js |
| 287 | +const { start } = require('bindings')('clock'); |
| 288 | + |
| 289 | +start(function () { |
| 290 | + console.log("JavaScript callback called with arguments", Array.from(arguments)); |
| 291 | +}, 5); |
| 292 | +``` |
| 293 | + |
| 294 | +When executed, the output will show the value of `clock()` five times at one |
| 295 | +second intervals: |
| 296 | + |
| 297 | +``` |
| 298 | +JavaScript callback called with arguments [ 84745 ] |
| 299 | +JavaScript callback called with arguments [ 103211 ] |
| 300 | +JavaScript callback called with arguments [ 104516 ] |
| 301 | +JavaScript callback called with arguments [ 105104 ] |
| 302 | +JavaScript callback called with arguments [ 105691 ] |
| 303 | +``` |
0 commit comments