Repository navigation
Conversation
…free_thread` at thread exit SAPI's should already been doing this on their own before, but the apache2handler doesn't get control over threads, so it didn't call ts_free_thread and therefore started leaking resources with the move of per-thread globals from heap storage to native __thread storage
| } | ||
| if (thread_resources && !tsrm_thread_exit_registered) { | ||
| tsrm_thread_exit_registered = true; | ||
| _tlv_atexit(tsrm_thread_exit_handler, NULL); |
There was a problem hiding this comment.
Nit: Could this we moved next to the pthread_key_create() call? Purely for clarity / consistency
There was a problem hiding this comment.
No, https://github.com/apple-oss-distributions/dyld/blob/main/include/mach-o/dyld.h#L104 documents that _tlv_atexit is for the calling thread, while the pthread_key_create is once for the process. We don't want to register for the main thread, but for the other threads spawned later.
| return; | ||
| } | ||
| data->thread_resources = thread_resources; | ||
| if (!FlsSetValue(tsrm_exit_key, data)) { |
There was a problem hiding this comment.
Could we use FlsSetValue(tsrm_exit_key, thread_resources) directly? This would simplify the code substantially. If we can't, please add a comment to hint at why we use a separate struct.
There was a problem hiding this comment.
Added a comment why we can't, we need some kind of identity mechanism because per-fiber (fls callbacks) is not per-thread (tsrm resources) and a fiber teardown could otherwise tear down a threads resources.
There was a problem hiding this comment.
/* FLS is per-fiber, but TSRM resources are per-thread. Another fiber can free * and reallocate the resources while this fiber retains its FLS value. Keep a * separate record so callbacks can distinguish stale FLS values even if the * resource allocation reuses the same address. */
Let me know if I understand the concern here: We assume that ts_resource_ex() / allocate_new_resource() will be called from a thread, but that thread may be converted to a fiber later, which implies a few things:
- The fiber may outlive out the thread and the related TLS
- The fiber may be terminated before the thread
- The fiber may execute or terminate on another thread
All of these cases will cause ts_free_thread to not be called, or to be called too early (2nd case).
Do we really want to support this? We should probably add some !IsThreadAFiber() assertions and ignore those cases.
Are there other "on thread terminate callback" mechanism we could use instead? Maybe we could use tss_create() or PIMAGE_TLS_CALLBACK?
| free(data); | ||
| return; | ||
| } | ||
| if (FlsGetValue(tsrm_exit_key) != data) { |
There was a problem hiding this comment.
I may we wrong, but I think that we can simplify this to if (!data) {: If we are called because a Fiber exited, we receive that Fiber's specific data in arg, so it should be NULL?
There was a problem hiding this comment.
But that's the wrong check, if !data would check if the thread or fiber doesn't have a value set. Here we want to compare if the value is the same one as that of our exit key. It could also be a fiber deleting another in which case data != NULL.
| return; | ||
| } | ||
| if (FlsGetValue(tsrm_exit_key) != data) { | ||
| free(data); |
There was a problem hiding this comment.
I think that freeing data in this case is wrong
There was a problem hiding this comment.
We're only freeing the fibers cleanup record here, not the actual resources (being data->thread_resources). Msdn says the callback is for freeing (that?) memory so I do believe it's correct.
There was a problem hiding this comment.
(We allocate the cleanup record in tsrm_atexit_set_data())
| do { | ||
| zend_try { | ||
| zend_destroy_rsrc_list(&EG(persistent_list)); | ||
| completed = true; | ||
| } zend_end_try(); | ||
| } while (!completed); |
There was a problem hiding this comment.
The catch/loop seem unrelated to the current PR?
There was a problem hiding this comment.
Hah I recall this one, had to add it because the persistent_list destructors can bailout.
There was a problem hiding this comment.
Could you split this out of this PR? Is this necessary at all? Not a fan of catching bailouts.
There was a problem hiding this comment.
Could apply it later, but not before this PR, because persistent_list cleanup just doesn't happen on threads calling ts_free_thread on master now. I think it belongs in the PR, because it's really part of its handling.
| - Per-thread TSRM resources are now released best-effort at normal thread exit, | ||
| because native TLS resource destructors can only run on their owning thread and | ||
| tsrm_shutdown() cannot release them afterwards. Prefer explicitly calling ts_free_thread(). |
There was a problem hiding this comment.
Is that still true? In which case are resources not released?
There was a problem hiding this comment.
It's best-effort, pthread_key_create of FlsAlloc could fail and we don't want to fail php if a callback registration fails. Someone could also deliberately detach exit handlers before a thread exits... but yeah that's probably not worth mentioning.
|
@arnaud-lb did the refactoring you asked for, I think for the other comments the code is right as is. |
| if (!tsrm_thread_exit_armed) { | ||
| fprintf(stderr, "TSRM: pthread_key_create() failed (error %d); automatic thread cleanup is disabled\n", error); | ||
| } |
There was a problem hiding this comment.
Can we return bool from this function and let the caller handle the logging?
There was a problem hiding this comment.
A bool would miss the reason. A char* (buffer) maybe. But then we'd still assemble the string in the atexist function and the benefit is gone.
| return; | ||
| } | ||
| data->thread_resources = thread_resources; | ||
| if (!FlsSetValue(tsrm_exit_key, data)) { |
There was a problem hiding this comment.
/* FLS is per-fiber, but TSRM resources are per-thread. Another fiber can free * and reallocate the resources while this fiber retains its FLS value. Keep a * separate record so callbacks can distinguish stale FLS values even if the * resource allocation reuses the same address. */
Let me know if I understand the concern here: We assume that ts_resource_ex() / allocate_new_resource() will be called from a thread, but that thread may be converted to a fiber later, which implies a few things:
- The fiber may outlive out the thread and the related TLS
- The fiber may be terminated before the thread
- The fiber may execute or terminate on another thread
All of these cases will cause ts_free_thread to not be called, or to be called too early (2nd case).
Do we really want to support this? We should probably add some !IsThreadAFiber() assertions and ignore those cases.
Are there other "on thread terminate callback" mechanism we could use instead? Maybe we could use tss_create() or PIMAGE_TLS_CALLBACK?
I'm not sure. Technically yes, but I don't think php, frankenphp or ext-parallel have any path causing it. Worst case I think cleanup would be missed, hence the recommendation to call
Yes. [...] No, that's what we have the guard for that you asked about before. So it doesn't clean up too early.
Yes, see point 1.
Not particularly, but we don't want to crash when someone does silly things ;). If you want assertions (or, realistically, runtime checks, because whoever will use php downstream probably doesn't use a debug build), we can do that instead of the existing "best-effort" guards.
Afaik they run under DllMain locks and experience has taught me that you never want to do any sort of cleanup there. |
I would prefer that yes, because I'm having a very hard time understanding the current guards. This is adding a lot of complexity for an hypothetical use-case that we can't properly support anyway. |
|
Shut the pc down for the day, but sounds reasonable to me. @shivammathur I take it you have no immediate plans to use native fibers? 😆 |
|
Yes, no native Windows fiber requirement from my side. I agree with @arnaud-lb. |
... to automatically call
ts_free_threadat thread exitSAPI's should already been doing this on their own before, but the apache2handler doesn't get control over threads, so it didn't call ts_free_thread and therefore started leaking resources with the move of per-thread globals from heap storage to native __thread storage
@shivammathur's requests in #23227
Will need minimal adjusting in the eg/cg move later, but it can already be merged standalone.
cc @arnaud-lb