-
Notifications
You must be signed in to change notification settings - Fork 5.4k
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
feat(ext/ffi): Thread safe callbacks #14942
Conversation
…one synchronously or with message passing
cli/dts/lib.deno.unstable.d.ts
Outdated
@@ -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 |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
@@ -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)); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
One question that I thought of and tested that locally this caused no issues is the response This seems unnecessary, as the 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 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. |
There was a problem hiding this 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.
Good point. The vector allocation might cause perf regression. |
Fairly okay working solution to thread safe FFI callbacks, complete with tests!
A new
FfiState
is saved into theOpState
, containing the sender and receiver for astd::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 onUnsafeCallback
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 mainSyncChannel
. The message is a boxed closure which will call the actualdeno_ffi_callback
internal implementation and then send a message into the newly created responseSyncChannel
. The foreign thread will then block on the responseSyncChannel
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:
UnboundedChannel
sync_channel
bound set to 0, making it a rendezvous channel