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

Threadsafe Handles #32

Closed
wants to merge 6 commits into from
Closed

Threadsafe Handles #32

wants to merge 6 commits into from

Conversation

kjvalencik
Copy link
Member

Two new Send + Sync features are introduced: EventQueue and Persistent. These features work together to allow multi-threaded modules to interact with the Javascript main thread.

  • EventQueue: Threadsafe handle that allows sending closures to be executed on the main thread.
  • Persistent<T>: Opaque handle that can be sent across threads, but only dereferenced on the main thread.

fn log(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let persistent_cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx);

// `Persistent` are reference counted and clone-able.

This comment was marked as resolved.


```rust
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx);

This comment was marked as resolved.

- ~A global `EventQueue` is necessary for dropping `Persistent`. Should this be exposed?~ No. This is no longer required for the design.
- ~The global `EventQueue` requires instance data. Are we okay using an [experimental](https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis) API?~ This is no longer required for the design.
- Should `EventQueue::send` accept a closure that returns `()` instead of `NeonResult<()>`? In most cases, the user will want to allow `Throw` to become an `uncaughtException` instead of a `panic` and `NeonResult<()>` provides a small ergonomics improvement.
- `persistent(&mut cx)` is a little difficult to type. Should it have a less complicated name?

This comment was marked as resolved.


Two new `Send` and `Sync` features are introduced: `EventQueue: Send + Sync` and `Persistent: Send`. These features work together to allow multi-threaded modules to interact with the JavaScript main thread.

* `EventQueue`: Threadsafe handle that allows sending closures to be executed on the main thread.

This comment was marked as resolved.

Two new `Send` and `Sync` features are introduced: `EventQueue: Send + Sync` and `Persistent: Send`. These features work together to allow multi-threaded modules to interact with the JavaScript main thread.

* `EventQueue`: Threadsafe handle that allows sending closures to be executed on the main thread.
* `Persistent<T>`: Opaque handle that can be sent across threads, with inner contents only accessible on the main thread.

This comment was marked as resolved.

This comment was marked as resolved.

let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx);

std::thread::spawn(move || {
// Panics when `cb` is dropped.

This comment was marked as resolved.

}
```

While `Persistent<_>` may be shared across threads, the inner contents can only be accessed on the main thread.

This comment was marked as resolved.


### `neon::handle::EventQueue`

Once a value is wrapped in a `Persistent<_>` handle, it must be sent back to the main JavaScript thread to unwrap. `EventQueue` provides a mechanism for requesting work be peformed on the main thread.

This comment was marked as resolved.

This comment was marked as resolved.

```rust
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx);
let queue = EventQueue::new(&mut cx)?;

This comment was marked as resolved.

}
```

`Persistent<_>` are clone-able and can be used many times. `EventQueue` are `Sync` and may be shared across threads.

This comment was marked as resolved.


```rust
fn thread_callback(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let cb = cx.argument::<JsFunction>(0)?.persistent(&mut cx);

This comment was marked as resolved.

This comment was marked as resolved.

let queue = Arc::new(EventQueue::new(&mut cx)?);

let handles = (1..=10).into_iter().map(|i| {
let queue = Arc::clone(queue);

This comment was marked as resolved.

This comment was marked as resolved.


Instances of `EventQueue` will keep the event loop running and prevent the process from exiting. The `EventQueue::unref` method is provided to change this behavior and allow the process to exit while an instance of `EventQueue` still exists.

However, calls to `EventQueue::schedule` _might_ not execute before the process exits.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

The native `napi_call_threadsafe_function` can fail in three ways:

* `napi_queue_full` if the queue is full
* `napi_invalid_arg` if the thread count is zero.ß This is due to misuse and statically enforced by ownership rules and the `Drop` trait.

This comment was marked as resolved.

This comment was marked as resolved.


Neon already has `Task`, `EventHandler`, a [proposed](https://github.com/neon-bindings/rfcs/pull/30) `TaskBuilder` and an accepted, but currently unimplemented, [update](https://github.com/neon-bindings/rfcs/pull/28) to `EventHandler`. This is a large amount of API surface area without clear indication of what a user should use.

This can be mitigated with documentation and soft deprecation of existing methods as we get a clearer picture of what a high-level, ergonomic API would look like.

This comment was marked as resolved.

@dherman
Copy link
Contributor

dherman commented Sep 26, 2020

Finished my review! This is going to be a great addition to the core primitives of Neon. I'm psyched.


/// Prevent the Node event loop from existing while this `EventQueue` exists. (Default)
/// _Idempotent_
pub fn reference(&mut self);

This comment was marked as resolved.


Both `ref` and `unref` mutate the behavior of the underlying threadsafe function and require exclusive references. These methods are infallible because Rust is capable of statically enforcing the invariants. We may want to optimize with `assert_debug!` on the `napi_status`.

#### Future Expansion

This comment was marked as off-topic.

@goto-bus-stop goto-bus-stop changed the base branch from master to main October 8, 2020 13:46
@kjvalencik kjvalencik force-pushed the kv/threadsafe-handles branch from be6c27a to 81f2bab Compare October 9, 2020 19:22
kjvalencik added a commit to neon-bindings/neon that referenced this pull request Jan 5, 2021
feat: Implements Threadsafe Handles as specified by neon-bindings/rfcs#32

Requires the `event-queue-api` feature flag until the RFC is accepted.
Copy link
Contributor

@dherman dherman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's just a couple things out of date now, but otherwise I think this is probably ready for Final Comment Period.

@kjvalencik kjvalencik added the final comment period last opportunity to discuss the proposed conclusion label May 5, 2021
@kjvalencik kjvalencik removed the final comment period last opportunity to discuss the proposed conclusion label Jul 2, 2021
@kjvalencik
Copy link
Member Author

@dherman This is close to stabilization. One change I would like to make prior to merging is the addition of a return value. Two APIs to take inspiration from:

In both of these APIs, a JoinHandle is returned. On a thread::spawn, a user can call join to block until the thread exits and get the return value. Similarly, in tokio, the handle can be awaited to get the resolved value.

I propose the following changes:

struct Channel
    pub fn send<T, F>(&self, f: F) -> JoinHandle<T>
    where
        T: Send + 'static,
        F: FnOnce(TaskContext) -> NeonResult<T>;

We don't need to initially do anything with the returned JoinHandle, but by adding it there, it allows extension without breaking changes to introduce JoinHandle::join and impl Future for JoinHandle.

Also, I'm still not feeling certain send is the right choice. Maybe it should be renamed spawn? If it is renamed spawn, Channel probably isn't the best name either.

@dherman
Copy link
Contributor

dherman commented Sep 3, 2021

This is a great idea @kjvalencik.

I think I do like send better than spawn, though, because we aren't spawning a thread (not even conceptually like a green thread), the user should be thinking of it as sending information back to an existing thread. As far as mixing traditions (channel / join) I think I'm comfortable with it: conceptually we're dealing with a channel because we're communicating between threads, and conceptually we're dealing with joining because we're blocking on another thread's computation.

kjvalencik added a commit to neon-bindings/neon that referenced this pull request Sep 3, 2021
kjvalencik added a commit to neon-bindings/neon that referenced this pull request Sep 3, 2021
kjvalencik added a commit to neon-bindings/neon that referenced this pull request Sep 3, 2021
@kjvalencik kjvalencik requested a review from dherman September 3, 2021 21:36
@kjvalencik kjvalencik added the final comment period last opportunity to discuss the proposed conclusion label Sep 3, 2021
Copy link
Contributor

@dherman dherman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JoinHandle changes look good!

@kjvalencik
Copy link
Member Author

Merged in c639681

@kjvalencik kjvalencik closed this Mar 7, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
final comment period last opportunity to discuss the proposed conclusion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants