-
Notifications
You must be signed in to change notification settings - Fork 7.9k
True async api stable #19142
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
base: master
Are you sure you want to change the base?
True async api stable #19142
Conversation
Initial version of the asynchronous API for PHP. Includes only the API itself and the necessary core changes required for the API to function.
…ized into a method for retrieving any ClassEntry. Now, using this API function, you can obtain the required class entry by a type descriptor type.
Added API functions for coroutine context management: - zend_async_context_set_t: set value by string or object key - zend_async_context_get_t: get value by string or object key - zend_async_context_has_t: check if key exists - zend_async_context_delete_t: delete key-value pair Includes convenience macros for easier usage: ZEND_ASYNC_CONTEXT_SET_STR/OBJ, GET_STR/OBJ, HAS_STR/OBJ, DELETE_STR/OBJ
Added complete Context API implementation: - Updated zend_async_scheduler_register signature to accept context functions - Added context function pointers and registration in zend_async_API.c - Context API supports both string and object keys - Includes convenience macros for easy usage This completes the core API infrastructure for coroutine context management.
- Remove separate context_set, context_get, context_has, context_delete functions - Add single zend_async_new_context_fn function to create context instances - Move context implementation to ext/async module using zend_async_context_t structure - Update scheduler registration to include new_context_fn parameter - Add context field to zend_async_globals_t and zend_coroutine_s - Simplify Context API macros to ZEND_ASYNC_NEW_CONTEXT and ZEND_ASYNC_CURRENT_CONTEXT
% removal of the current Scope from the global structure
… always needs to be explicitly known. Macros like ZEND_ASYNC_CURRENT_SCOPE were updated. A new macro ZEND_ASYNC_MAIN_SCOPE was added for the main Scope.
…main coroutine can correctly return control.
* add macro START_REACTOR_OR_RETURN for reactor autostart
… is now passed to the main coroutine’s finalize method instead of being displayed on screen. * Fixed an issue with correctly passing the exception into the coroutine.
… captures the exception, marking it as handled in another coroutine.
ee7eb25
to
9486c42
Compare
ZEND_API bool zend_async_reactor_register(char *module, bool allow_override, | ||
zend_async_reactor_startup_t reactor_startup_fn, | ||
zend_async_reactor_shutdown_t reactor_shutdown_fn, | ||
zend_async_reactor_execute_t reactor_execute_fn, | ||
zend_async_reactor_loop_alive_t reactor_loop_alive_fn, | ||
zend_async_new_socket_event_t new_socket_event_fn, | ||
zend_async_new_poll_event_t new_poll_event_fn, | ||
zend_async_new_timer_event_t new_timer_event_fn, | ||
zend_async_new_signal_event_t new_signal_event_fn, | ||
zend_async_new_process_event_t new_process_event_fn, | ||
zend_async_new_thread_event_t new_thread_event_fn, | ||
zend_async_new_filesystem_event_t new_filesystem_event_fn, | ||
zend_async_getnameinfo_t getnameinfo_fn, zend_async_getaddrinfo_t getaddrinfo_fn, | ||
zend_async_freeaddrinfo_t freeaddrinfo_fn, zend_async_new_exec_event_t new_exec_event_fn, | ||
zend_async_exec_t exec_fn, zend_async_new_trigger_event_t new_trigger_event_fn) |
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.
Might make sense to put this into a struct so it can be extended without necessarily breaking the api.
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 did a partial review so far. I have at least one important comment, but otherwise this looks great. I hope that True Async eventually passes.
However I'm not convinced that this should go into core yet, for the following reasons:
- The C API is large and reflects the same concepts and behaviors as the userland API, while ext/async is the backend. So further discussions on the userland APIs will necessitate changes in the C API as well.
- If we iterate on the API in the 8.5 branch, this will create incompatibilities between different versions of the extension and different PHP releases. People will have a hard time testing the extension, which is the main goal of merging the API into core early. If we don't iterate in the 8.5 branch, then we don't need to merge the C API into 8.5.
- There are no tests, so this code will eventually break during maintenance of 8.5 and master.
- There are actually very few changes in core, so it seems that we could add a few extension points in core, and move most of this code to the extension. Then it will be easier to iterate, and for people to test. These extension points could take the form of function pointers. For example we could make
shutdown_destructors
a function pointer. - I understand that we don't expect other extensions to try to integrate with the API. Therefore the API can be in ext/async, and we don't need this amount of decoupling and indirection.
static bool php_output_main_coroutine_start_handler( | ||
zend_coroutine_t *coroutine, bool is_enter, bool is_finishing) |
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.
From this example alone I don't fully understand the use-case. The code is called exactly once in the lifetime of the coroutine, seemingly for one specific coroutine. It feels like it could be executed at the spawn site.
typedef struct { | ||
zend_async_timer_event_t event; | ||
uv_timer_t uv_handle; | ||
} async_timer_event_t; | ||
// Initialize callbacks for the event | ||
event->event.base.add_callback = libuv_add_callback; | ||
event->event.base.del_callback = libuv_remove_callback; | ||
event->event.base.start = libuv_timer_start; | ||
event->event.base.stop = libuv_timer_stop; | ||
event->event.base.dispose = libuv_timer_dispose; |
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 was confused at first, thinking these were libuv functions. Prefixing global names with zend_
, php_
, or something else, make the origin of the symbol less ambiguous, and also avoid clashes. As these are defined in ext/async, I would suggest php_async_
: php_async_libuv_add_callback
. We usually prefix local (static) symbols too, sometimes with a shorter prefix (e.g. phpa_
for php async).
Every extended event defines its own ``start``, ``stop`` and ``dispose`` functions. The dispose | ||
handler must release all resources associated with the event and is called when the reference count | ||
reaches ``1``. It is common to stop the event first and then close the underlying libuv handle so |
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.
reaches ``1``. It is common to stop the event first and then close the underlying libuv handle so | |
is ``1``. It is common to stop the event first and then close the underlying libuv handle so |
or
reaches ``1``. It is common to stop the event first and then close the underlying libuv handle so | |
reaches ``0``. It is common to stop the event first and then close the underlying libuv handle so |
@@ -1929,6 +1930,8 @@ void php_request_shutdown(void *dummy) | |||
zend_call_destructors(); | |||
} zend_end_try(); | |||
|
|||
ZEND_ASYNC_RUN_SCHEDULER_AFTER_MAIN(); |
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.
This is worth a comment to explain that this is called to handle any task created by shutdown functions or destructors, and to handle destructors that didn't return yet.
Possibly this should be executed between php_call_shutdown_functions() and zend_call_destructors() as well, so that destructors are not called before tasks created by php_call_shutdown_functions() are executed.
uint32_t zend_async_internal_context_key_alloc(const char *key_name) | ||
{ | ||
#ifdef ZTS | ||
tsrm_mutex_lock(zend_async_context_mutex); | ||
#endif |
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.
It would make sense to allow usage of zend_async_internal_context_key_alloc()
only during the startup phase, so that no mutex is needed.
if (callback->ref_count > 1) { | ||
// If the callback is still referenced, we cannot dispose it yet | ||
callback->ref_count--; | ||
return; | ||
} else if (UNEXPECTED(callback->ref_count == 0)) { | ||
// Circular free from destructor | ||
return; | ||
} else { | ||
ZEND_ASSERT(callback->ref_count > 0 | ||
&& "Callback ref_count must be greater than 0. Memory corruption detected."); | ||
} |
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.
This seems inconsistent with zend_async_event_t.dispose
and could lead to confusion:
- In
zend_async_event_t.dispose
is supposed to be called only when the event is not referenced anymore. It frees the event unconditionally. (According to docs.) - In
zend_async_event_callback_t.dispose
(or at least this implementation) manages the refcount and frees the callback only when it reaches zero.
|
||
if (event != NULL && waker != NULL) { | ||
// Find the trigger for this event | ||
zval *trigger_zval = zend_hash_index_find(&waker->events, (zend_ulong) event); |
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.
When using pointers as hash key, consider using rotr3() (Because zend_hash
doesn't hash numeric indices, it only masks them. This leads to collisions as the lower 3 bits are usually zero on pointers.)
|
||
if (event != NULL && waker != NULL) { | ||
// Find the trigger for this event | ||
zval *trigger_zval = zend_hash_index_find(&waker->events, (zend_ulong) event); |
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.
Nit:
zval *trigger_zval = zend_hash_index_find(&waker->events, (zend_ulong) event); | |
zend_async_waker_trigger_t *trigger = zend_hash_index_find_ptr(&waker->events, (zend_ulong) event); |
@@ -274,6 +305,11 @@ typedef struct _zend_gc_globals { | |||
uint32_t dtor_end; | |||
zend_fiber *dtor_fiber; | |||
bool dtor_fiber_running; | |||
gc_async_context_t async_context; /* async context for gc */ | |||
gc_stack *gc_stack; /* local mark/scan stack */ | |||
zend_coroutine_t *dtor_coroutine; |
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 suggest renaming to gc_coroutine
if the entire GC runs in it, not only destructors
ZEND_API int zend_gc_collect_cycles(void) | ||
{ | ||
int total_count = 0; | ||
bool should_rerun_gc = 0; | ||
bool did_rerun_gc = 0; | ||
if (UNEXPECTED(ZEND_ASYNC_IS_ACTIVE && ZEND_ASYNC_CURRENT_COROUTINE != GC_G(dtor_coroutine))) { | ||
|
||
if (GC_G(dtor_coroutine)) { | ||
return 0; | ||
} | ||
|
||
start_gc_in_coroutine(); |
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.
The new behavior seems complex. Let me know if I understand it correctly:
zend_gc_collect_cycles()
is now asynchronous whenZEND_ASYNC_IS_ACTIVE
. It creates a coroutine (GC_G(dtor_coroutine)
) and returns. (This implies that the userland functiongc_collect_cycles()
is async as well and always return0
.)- The coroutine re-enters
zend_gc_collect_cycles()
, but this time it doesn't create a coroutine as it detects that it's running inGC_G(dtor_coroutine)
already - The coroutine creates a microtask that re-creates the coroutine in case the first one suspends. This re-enters
zend_gc_collect_cycles()
again. - Some of
zend_gc_collect_cycles
local variables are moved toGC_G()
because they are shared between multiple invocations
I understand the intent is that if a destructor suspends, we continue in an other coroutine.
I'm not entirely sure of the priority that is given to the GC: Are coroutines added to the front or back of the queue? (I don't see where it is enqueued.) Does the microtask execute immediately after the coroutine is suspended?
I think it's not necessary that the entire GC runs in a coroutine: the scanning part is inherently synchronous, non-reentrant, must not be interrupted, and does not call any userland code; so it's not necessary and it also does not improve concurrency. It is enough that just the "Actually call destructors" part runs in a coroutine, like we do for fibers.
Can this be simplfied like this?
- Run
zend_gc_collect_cycles()
synchronously, outside of a coroutine, as usual - Invoke destructors from a coroutine if necessary, and call the rest of the destructors in another one in case a destructor suspends
- This process should not allow
zend_gc_collect_cycles()
to return before all destructors have been invoked, so it's not necessary to move its local variables toGC_G()
. (Returning before all destructors have returned is fine.)
It cannot go into 8.5 in any case as the RFC vote has not started yet and the deadline for the start has already passed. |
TrueAsync engine API
The TrueAsync engine API defines a pluggable interface that lets extensions register different async backends while the core supplies standardized primitives.
Key Components
PR for https://wiki.php.net/rfc/true_async_engine_api