Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat(ext/ffi): Thread safe callbacks #14942

Merged
merged 23 commits into from
Jun 28, 2022

Conversation

aapoalas
Copy link
Collaborator

@aapoalas aapoalas commented Jun 22, 2022

Fairly okay working solution to thread safe FFI callbacks, complete with tests!

A new FfiState is saved into the OpState, containing the sender and receiver for a std::mpsc::sync_channel which is used for message passing between threads. Isolate threads (main and worker) are now identified using a thread-local Isolate pointer which gets assigned to the thread on UnsafeCallback creation. This allows an incoming callback to not only figure out if the thread it is being called on is an isolate thread but to also tell apart its own isolate thread from others, thus making it possible for workers to register and trigger callbacks originating in any thread while still ensuring that those callbacks are called on the appropriate isolate thread.

If the callback is found to be coming from a foreign thread (foreign isolate thread or completely foreign), it will create a new response SyncChannel and sends a message using its own copy of the sender side of the main SyncChannel. The message is a boxed closure which will call the actual deno_ffi_callback internal implementation and then send a message into the newly created response SyncChannel. The foreign thread will then block on the response SyncChannel until it receives the response message after which it unblocks the thread and deallocates the response channel and returns control to the caller of the callback.

Open questions:

  • Channel type decided in the favour of an UnboundedChannel
  • Isolate pointer comparison deemed apt, or at least not objected to
  • Response sync_channel bound set to 0, making it a rendezvous channel
  • Error handling for message passing?

@aapoalas aapoalas changed the title Implement support for thread safe FFI callbacks feat(ext/ffi): Thread safe callbacks Jun 23, 2022
ext/ffi/lib.rs Outdated Show resolved Hide resolved
ext/ffi/lib.rs Outdated Show resolved Hide resolved
@@ -554,6 +554,9 @@ declare namespace Deno {
* as C function pointers to ffi calls.
*
* The function pointer remains valid until the `close()` method is called.
*
* The callback can be explicitly referenced and dereferenced to stop Deno's
Copy link
Member

Choose a reason for hiding this comment

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

I believe dereferenced is misleading in this context

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Any suggestion what might be a better term? I how Node documents their ref and unref functions and, surprise surprise, they use the verbs ref and unref and term ref'd (or maybe ref'ed).

Should I just align with that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Used ref'ed and unref'ed for now.

ext/ffi/lib.rs Outdated Show resolved Hide resolved
ext/ffi/lib.rs Outdated Show resolved Hide resolved
ext/ffi/lib.rs Outdated
@@ -1096,11 +1179,16 @@ where
let cb = v8::Local::<v8::Function>::try_from(v8_value)?;

let isolate: *mut v8::Isolate = &mut *scope as &mut v8::Isolate;
LOCAL_ISOLATE_POINTER.with(|s| s.replace(isolate));
Copy link
Member

Choose a reason for hiding this comment

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

Maybe add a check that we've replaced a null pointer here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added a check to only replace the null pointer once.

ext/ffi/lib.rs Outdated Show resolved Hide resolved
@aapoalas
Copy link
Collaborator Author

aapoalas commented Jun 27, 2022

One question that I thought of and tested that locally this caused no issues is the response sync_channel. Currently I create it with a bound of 1, meaning that if somehow theoretically the isolate thread manages to call the do_ffi_callback and then send a response before the "async" thread calls response_receiver.recv().unwrap() then the response can be buffered.

This seems unnecessary, as the sync_channel receiver side is literally this:

async_work_sender.unbounded_send(fut).unwrap(); // Send the message
response_receiver.recv().unwrap(); // Block on a response

The isolate thread would need to be lightning fast to poll the async work channel and do the work, and the calling thread would need to be absolutely massively busy to not get to call .recv() before the response sent. This does not seem to be a particularly realistic case.

As such the bound could perhaps be lowered to 0, making the channel a "rendezvous channel" which brings with it some perf optimisations I believe.

Opinions?

EDIT: I indeed changed this to a rendezvous channel with blocking on both ends for the other side to become available. This means that even if the receiving thread is very fast and the sending thread is very busy, they'll wait for each other on the channel and pass the response directly without using the buffer.

Copy link
Member

@littledivy littledivy left a comment

Choose a reason for hiding this comment

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

LGTM. This introduces an event loop middleware - let's watch for any unrelated perf regressions.

@aapoalas
Copy link
Collaborator Author

LGTM. This introduces an event loop middleware - let's watch for any unrelated perf regressions.

Good point. The vector allocation might cause perf regression.

@littledivy littledivy merged commit 00f4521 into denoland:main Jun 28, 2022
dsherret pushed a commit to dsherret/deno that referenced this pull request Jun 30, 2022
@aapoalas aapoalas deleted the feat/thread-safe-callbacks branch July 10, 2022 08:55
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants