How does GC work w.r.t thread-local-globals? #34
Replies: 7 comments 48 replies
-
I know @conrad-watt has thought a lot about this in the context of the thread-local JS function wrappers as well. Also cc @syg in case you've already thought about this and arrived at a conclusion for the JS side of things. |
Beta Was this translation helpful? Give feedback.
-
@lukewagner The sketch I've thought about is your second option. The first option doesn't work because it's very surprising if TLS is programmatically available, for the scenario you've discovered. To your two points, it is correct that (1) TLS fundamentally introduces a restricted kind of shared -> unshared edge, where, because the per thread nature of TLS, the shared -> unshared edge cannot escape the current thread that is viewing the edge. It is also the case that this requires at least a marking phase that can view the object closure of all threads. The semantics of TLS corresponds exactly to a per-thread JS WeakMap (i.e. ephemerons) where the keys are shared. To collect those ephemerons, the shared keys have to die, and since they're shared, that means they're unreachable from every thread. Edit: The only phases of GC that requires a global view of the world is marking + nulling of dead weak ephemerons. Actual sweeping, compaction, etc, can proceed independently. This is of course a complication, but does not require a full global GC for every phase. I hear and share some of the leak worries you point out in (2). I don't know if I'd strictly think about it as a leak but as an optimization opportunity that we can explore. Somewhat analogous to this, IMO, is the closure entrainment problem in JS closures. If an outer function has closed-over bindings
What are you thinking of here? Manual management can be done in the automatic TLS world as well by nulling out your own thread's cell, just as one can work around the closure entrainment problem by nulling out unused upvars. |
Beta Was this translation helpful? Give feedback.
-
This seems a bit unfortunate. I had thought that the design of
Despite liking the attractiveness of having a single unified "thread-local global" concept, I was thinking of splitting out the two concrete problems we have:
I think the solution to both involves avoiding any shared→unshared GC edges, but in different ways owing to the different constraints:
So digging into the solutions I was imagining, at a high-level:
Sorry for the not-succinct-yet-not-complete answer; I mostly wanted to try to lay out the alternative in enough detail to contrast with the thread-local approach. |
Beta Was this translation helpful? Give feedback.
-
If I understand the high-order bit here, @lukewagner is concerned about requiring GCs to support shared-to-unshared edges. As @syg mentioned, these edges are already allowed without any kind of TLS if you allow shared objects to be keys in |
Beta Was this translation helpful? Give feedback.
-
@lukewagner Starting a separate thread for the clarifying question. 2 questions:
Edit: The only answer I have to (2) is "we'll give you less expressivity by also disallowing shared objects as keys in WeakMaps". That strikes me as a bad position since it disallows composition due to the implementation limitations of today. |
Beta Was this translation helpful? Give feedback.
-
I know that I'm biased here, but I do believe that historically we have given precedence to engine difficulties over wasm expressiveness in the past. From SM's initial research into having the global TLS lifetime semantics (AKA shared objects as keys in weakmaps), we think there are some significant difficulties here (see my earlier comment about embedder's with cycle collectors). If there's no other way to progress, then that could be something we just figure out. But if we have other possible solutions, I think we need to investigate those first. |
Beta Was this translation helpful? Give feedback.
-
For those interested, I posted an alternative to thread-local globals and functions in #42. |
Beta Was this translation helpful? Give feedback.
-
I was trying to think through how a JS embedder's GC would define liveness w.r.t thread-local globals (including
WebAssembly.Function({ threadLocal: true })
) and it seemed like there were roughly two options, each with significant downsides. But maybe I'm missing another option?So in the first option, a thread-local-global's thread-local contents are only considered live if there is some reachable path from the Worker's (thread-local) roots to a wasm exported shared function (or shared table or any other shared wasm JS API object) whose entrained (instance) closure state points to that thread-local-global. The downside of this approach is that if wasm stores some state into a thread-local-global from a worker, and then the shared wasm instance becomes temporarily unreachable from that worker, and then a GC occurs, and then the shared wasm instance becomes reachable again on that worker (via some other worker that held onto a reference and sent it back over), the thread-local-global state can get observably reset.
To fix this, the second option is to say that a thread-local-global's entire contents (the cells of all threads) are considered live if the thread-local-global is reachable from a shared wasm instance reachable from any worker. Thus, in the above temporarily-unreachable scenario, the worker's thread-local state would not get reset because some other worker held onto a reference to the shared wasm instance. But now there are two downsides:
Assuming there's not a clean way out of this, I do wonder if maybe this is a signal that perhaps thread-local-globals aren't the right technical approach to the TLS problem -- that they put too much magic in the engine and that a lower-level approach might be appropriate. FWIW, I think a variation of the "thread-id" approach could avoid this problem (putting control in guest-code's hands instead).
Beta Was this translation helpful? Give feedback.
All reactions