2024-05-15 [07:14:30.0860] FYI: I should be at the meeting today, but I am waiting on an appliance repair technician with a fairly wide arrival window and may have to step away for a few minutes if they arrive during the meeting. [07:35:41.0925] thanks for the heads up [07:35:56.0567] my agenda is to address mark's comments at the last plenary re: methods [07:36:12.0527] so hopefully he can make the one today, which i unfortunately had to reschedule due to an off-site 2024-05-30 [19:39:46.0642] I chatted with Mark this afternoon. First he's sorry about not being able to make it this morning. From what I understand the biggest concern with adding prototype methods to shared structs is that it makes it too easy to transform existing single threaded code into code shared memory multi-threaded code without the author realizing the implication of such a transformation. This is especially true with non-shared structs also existing as you're roughly a "shared" keyword away from transforming into multithreaded existing but non thread safe code. Apparently this is an issue that Java and C# both suffered from. [19:40:09.0203] * I chatted with Mark this afternoon. First he's sorry about not being able to make it this morning. From what I understand the biggest concern with adding prototype methods to shared structs is that it makes it too easy to transform existing single threaded code into shared memory multi-threaded code without the author realizing the implication of such a transformation. This is especially true with non-shared structs also existing as you're roughly a "shared" keyword away from transforming into multithreaded existing but non thread safe code. Apparently this is an issue that Java and C# both suffered from. [20:54:46.0505] > <@mhofman:matrix.org> I chatted with Mark this afternoon. First he's sorry about not being able to make it this morning. From what I understand the biggest concern with adding prototype methods to shared structs is that it makes it too easy to transform existing single threaded code into shared memory multi-threaded code without the author realizing the implication of such a transformation. This is especially true with non-shared structs also existing as you're roughly a "shared" keyword away from transforming into multithreaded existing but non thread safe code. Apparently this is an issue that Java and C# both suffered from. is that in reference to `static` ? [20:55:22.0120] static? [21:07:57.0775] I think the problem is that code written without specific handling of shared memory access is unlikely to be safe when running in multiple threads. Java and C# do not prevent object instances from being shared in the first place, so the problem in these languages is arguably worse since it's pretty much not up to the implementer of the class to enable multi-threading (at best it can document that the class is not thread safe). The current shared struct proposal does require opt-in by marking the object type and/or methods as shared, but we consider that to not be a sufficient friction point in transforming non multi-threaded code, as it's highly unlikely that simply marking a method or type as shared to be sufficient, and that explicit locking logic is likely to be required as well. [21:11:13.0249] * I think the problem is that code written without specific handling of shared memory access is unlikely to be safe when running in multiple threads. Java and C# do not prevent object instances from being shared in the first place, so the problem in these languages is arguably worse since it's pretty much not up to the implementer of the class to enable multi-threading (at best it can document that the class is not thread safe). The current shared struct proposal does require opt-in by marking the struct type as shared, but we consider that to not be a sufficient friction point in transforming non multi-threaded code, as it's highly unlikely that simply marking a struct as shared to be sufficient, and that explicit locking logic is likely to be required as well in the methods. [21:24:38.0751] here's a wild idea, probably misguided as I arguably don't fully grasp the complexities of properly implementing safe shared memory concurrency. Would it make sense that by default (without some kind of explicit opt-out), all methods of a shared struct would take a thread local lock on the instance. By that I mean every time a shared struct method is invoked, it'd check if the thread already has a lock on the object (in case of local re-entrancy or simply the method being called from another method), and if not, acquire a lock on the object. While that's unlikely to be sufficient to reliably protect the users of the object, it should at least make the methods implementations thread safe by default. [21:30:05.0730] > <@mhofman:matrix.org> static? you mentioned java and c# -- I was asking if you are referring to the `static` keyword from those languages [21:31:07.0180] from what I understand there is plenty of ways in those languages to make object instances available to multiple threads, not just the `static` keyword [21:32:39.0709] sure. contextually, it seemed it was in reference to 'you're roughly a "shared" keyword away from transforming into multithreaded existing but non thread safe code' [21:34:07.0309] In general, we remain skeptical about introducing complexity just to enable developers to use shared object as regular objects with methods [21:34:34.0389] what I am trying to understand is what specific comparisons are being made to java and c# [21:35:44.0893] > <@softwarechris:matrix.org> sure. contextually, it seemed it was in reference to 'you're roughly a "shared" keyword away from transforming into multithreaded existing but non thread safe code' ah yeah. I think the point I was trying to make is that it's just too easy to cause code that isn't written with thread safety in mind to execute in multiple threads [21:36:33.0449] it certainly can be... ask me some time about how an errant `static` nearly brought down a company [21:36:59.0541] although java/c# folks will probably tell you that the ease of that is a feature rather than a bug [21:37:14.0243] * although java/c# folks will probably tell you that the ease of doing that is a feature rather than a bug [21:37:19.0893] the JS proposal is marginally better as it requires an opt-in from the object's implementor, but the "opt-in" is still too easy in our opinion [21:58:55.0552] the headers you mean? [22:17:38.0843] the headers are extremely hard to opt into, i don't understand [22:17:45.0367] mark would like more syntactic friction? [22:18:27.0313] i don't really understand how someone can accidentally opt into multitreading [22:18:41.0374] like, making the struct shared is a necessary but insufficient condition to actually opt into the style [22:18:51.0167] you have to communicate it to another thread, set up the code to receive it, etc [22:20:13.0126] this argument seems very weak to me [22:21:50.0789] > <@mhofman:matrix.org> ah yeah. I think the point I was trying to make is that it's just too easy to cause code that isn't written with thread safety in mind to execute in multiple threads this is true, and is not a goal of this proposal [22:23:12.0068] that is, it is not a goal of this proposal to be opinionated about a particular style of thread safety [22:25:00.0704] the syntactic friction argument doesn't hold water. if the headers aren't considered enough friction, i don't know what would be. if the headers are considered enough friction but wants it reflected at the engine level, we can choose to spec an opt-in gate that the host has to trigger, and it'll be up to Node and other runtimes to understand the intention here is that it's an opt-in feature [22:26:18.0373] > <@mhofman:matrix.org> here's a wild idea, probably misguided as I arguably don't fully grasp the complexities of properly implementing safe shared memory concurrency. Would it make sense that by default (without some kind of explicit opt-out), all methods of a shared struct would take a thread local lock on the instance. By that I mean every time a shared struct method is invoked, it'd check if the thread already has a lock on the object (in case of local re-entrancy or simply the method being called from another method), and if not, acquire a lock on the object. While that's unlikely to be sufficient to reliably protect the users of the object, it should at least make the methods implementations thread safe by default. that's a non-starter [22:26:30.0693] it is too costly [22:28:00.0017] Does it matter if the default is costly as long as there is a way to opt out of the default safety and gain performance? [22:29:06.0370] well, yes, the default is already safe (the headers aren't present by default) [22:29:58.0078] it also puts a requirement on implementations that there be a lock per object [22:31:04.0018] The concern in this case is not how hard it is for the application as a whole to adopt shared memory multithreading, but how not sufficiently hard it is to mark code that is not thread safe to "support" shared memory access. Namely add a shared keyword to a struct declaration. [22:31:20.0207] what's the counterargument to what i said above? [22:31:35.0154] adding the shared keyword is a necessary but insufficient condition [22:31:48.0258] you still have to write code to communicate a shared struct [22:32:34.0316] It's sufficient from the implementor of the struct. Your argument assumes the author of the app and of the struct is the same. [22:33:12.0149] the worry is the app author downloads a library, sees that it's marked as a shared struct, and assumes it's threadsafe, but the library is buggy and it is not threadsafe? [22:34:21.0652] what's different in this case vs an otherwise buggy library? [22:34:49.0513] The worry is that the library authors could believe they can support multithreading by simply adding a keyword to their objects, without taking time to understand what they're actually doing [22:35:17.0287] that is a fully generic argument that can apply to anything that requires expertise...? [22:36:26.0079] i'm on board with safe by default. i consider that status quo to have that because it requires the app author to do the opt in, not the library authors [22:37:14.0925] if the app author trusts the library authors, and that trust turned out incorrect, i see that as the normal cost of doing software development [22:37:40.0244] I don't know of programming concepts that are similarly hard to get right if not extremely careful. [22:38:26.0904] i can think of several [22:38:34.0294] manual memory management, asynchrony [22:38:51.0401] JITs (dynamic codegen) [22:39:24.0669] also, what's the cost to getting it wrong? [22:39:27.0038] it's not crashes [22:39:36.0801] JS doesn't really have manual memory management, and I'd argue that it's maybe too easy to shoot yourself in the foot with array buffers. [22:39:40.0140] it's something like "undefined values" [22:40:42.0222] what i'm trying to get at it is: i don't see a principle at work here for how many layers/kinds of friction is enough, if the opt-in headers aren't [22:41:11.0251] I agree that asynchrony and in particular re-entrancy during suspension is not always sufficiently understood. But it's easier to reason about thanks to the explicitness of await points [22:41:40.0471] i don't think "appeasing mark" is a good design principle for how much friction something should have [22:43:03.0482] i'd also like to better understand the consequences of getting this wrong [22:43:07.0495] this = a buggy library [22:43:22.0047] why is that assumed to be a categorically worse kind of "wrong" than today's bugs? [22:43:26.0035] I have to go, sorry [22:44:14.0523] all right well, i'm pretty disappointed in the state of affairs [23:05:56.0632] Mathieu Hofman: here's a hypothetical when you're back. would making shared structs inaccessible outside of `shared { }` code blocks (a la `unsafe { }` blocks in rust) be considered sufficient syntactic friction? [23:06:44.0225] and if it isn't, i'd like to understand the reasoning [23:29:42.0055] > <@mhofman:matrix.org> I don't know of programming concepts that are similarly hard to get right if not extremely careful. `FinalizationRegistry` comes to my mind [23:40:35.0811] > <@shuyuguo:matrix.org> and if it isn't, i'd like to understand the reasoning I'll chat more with Mark [23:42:56.0753] > <@aclaymore:matrix.org> `FinalizationRegistry` comes to my mind That's actually a good example of a safer abstraction compared to destructors. Sure it's advanced, and still possible to create situations that are not optimal, but unlike destructors, it's a lot harder to cause critical bugs. [00:35:46.0101] SharedStructs are a safer abstraction than direct shared memory because there is no type-confusion as the fields don't overlap. [07:30:28.0978] FinalizationRegistry got right the thing where it prevents you from resurrecting dead objects, but it still seems to be abused most of the time :( [07:32:59.0891] > <@mhofman:matrix.org> In general, we remain skeptical about introducing complexity just to enable developers to use shared object as regular objects with methods This is a pretty broad thing to be skeptical of. How does this fit together with rbuckton's feedback that methods were important for usability? Also, are you considering that the fundamental technology ("TLS") is needed for Wasm anyway, so most of the complexity will be there in the system either way? [07:33:59.0327] also curious how this relates to having syntax for shared struct classes, which is all about reducing friction and something proposed to enhance usability [07:35:10.0668] this sort of "discourage people from using the feature" feedback seems to be pushing in the direction that the proposal was originally shaped in, where it was just some function calls that made some weird objects with null prototypes. I think that would be a worse design for JavaScript and I'm a big fan of the changes that have come over the past couple years. [07:36:02.0846] even though FinalizationRegistry uses a similarly function/constructor-based API with no syntax, that doesn't really provide any meaningful friction to prevent abuse. The motivation for abuse doesn't come from convenient syntax but rather useful semantics that people misunderstand and want to get at. [08:01:00.0127] > <@littledan:matrix.org> even though FinalizationRegistry uses a similarly function/constructor-based API with no syntax, that doesn't really provide any meaningful friction to prevent abuse. The motivation for abuse doesn't come from convenient syntax but rather useful semantics that people misunderstand and want to get at. this rings pretty true to me [15:41:43.0602] I haven't had a chance to catch up on this conversation since it started last night. I'll try to read through it and provide my thoughts tomorrow. [16:17:01.0509] If the concern is that there needs to be some kind of artificial barrier to using shared structs to discourage less-experienced developers from writing bad code, then one already exists. It is far more complex than just having a `shared` keyword, its completely out of band from the JS code itself, its something that requires domain knowledge to use correctly, and it already acts as a barrier against a number of different types of insecure code. You will need to enable COOP/COEP to be able to even use this feature on the web, just as you do for `SharedArrayBuffer`. That's a level of complexity far outside the domain of the average JS developer. [16:18:34.0210] Somehow special-casing shared struct methods to require a mandatory locking mechanism does nothing to ensure thread safety since it only affects shared struct methods, not the fields that are the actual unsafe things. [16:21:30.0745] I also absolutely do not want a repeat of `async`. While I absolutely love `async`/`await`, it is well established that introducing `await` often poisons your entire execution path with `async`. [16:27:03.0428] I also am very concerned of repeating the mistake of C#'s `lock` and Java's `synchronized` as they are both sledgehammers in a space where finesse is the correct approach, and both are often huge performance bottlenecks. [16:31:50.0111] That said, I'd find it perfectly reasonable to require something like an `unsafe` block/method/function to read or write from a struct field that is `writable: true`, such that the thread-safety risk is immediately observable to the author of the block/method/function, as it becomes up to the author of that code to reconcile how their code interacts with the surrounding code outside of that marked block. [16:51:17.0285] For example: ```js function doWork(sharedObj) unsafe { // allow unsafe read/write anywhere in the body const x = sharedObj.x; // ok sharedObj.x = y; // ok } function doWork2(sharedObj) { unsafe { // allow unsafe read/write anywhere in the block const x = sharedObj.x; // sharedObj.x = y; // ok } } function doWork3(sharedObj) { const x = unsafe sharedObj.x; // ok, but no parentheses allowed unsafe sharedObj.x = y; // ok, but no parentheses allowed } ``` [16:53:07.0287] For even more artificial ceremony, you could have a llnt rule that banned `unsafe` so you would be forced to disable the rule when needed (and hopefully document why). [16:59:30.0193] And we can make the basic Mutex easy to use with `using` if you really want/need the sledgehammer approach: ```js const mut = new Atomics.Mutex(); function doWork(sharedObj, mut) unsafe { using void = new UniqueLock(mut); // lock taken until unsafe block exits } ``` or even: ```js shared struct SharedObj { readonly mut = new Atomics.Mutex(); ... } function doWork(sharedObj) unsafe { using void = new UniqueLock(sharedObj.mut); } ``` or ```js shared struct SharedObj { readonly #mut = new Atomics.Mutex(); #x; #y; // using encapsulation, all access is governed by the lock doWork() unsafe { using void = new UniqueLock(this.#mut); const x = this.#x; const y = this.#y; return { x, y }; } } ``` 2024-05-31 [21:00:49.0847] > <@shuyuguo:matrix.org> Mathieu Hofman: here's a hypothetical when you're back. would making shared structs inaccessible outside of `shared { }` code blocks (a la `unsafe { }` blocks in rust) be considered sufficient syntactic friction? what do you mean by making inaccessible? I doubt you mean preventing interaction with instances of share structs outside these blocks? I don't see how could even work. [21:07:24.0490] > <@littledan:matrix.org> This is a pretty broad thing to be skeptical of. How does this fit together with rbuckton's feedback that methods were important for usability? Also, are you considering that the fundamental technology ("TLS") is needed for Wasm anyway, so most of the complexity will be there in the system either way? For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there. [21:14:35.0314] > <@littledan:matrix.org> even though FinalizationRegistry uses a similarly function/constructor-based API with no syntax, that doesn't really provide any meaningful friction to prevent abuse. The motivation for abuse doesn't come from convenient syntax but rather useful semantics that people misunderstand and want to get at. FinalizationRegistry is different enough from destructors that it forces you to rethink what you're actually doing. Of course that doesn't guarantee the author will get it right. Shared structs is an improvement over SAB for complex value types. However SAB did force you to think about what you were doing when coming from an object model. The concern here is that an author can too easily take a regular non shared aware class, and transform it into a shared struct, without really thinking about what they're doing. [04:58:44.0152] > <@mhofman:matrix.org> For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there. The prototype-attaching thing could be done by Proxy, if you have the TLS primitive. That is, it can be implemented just with what Wasm will already add. [05:58:42.0301] > <@mhofman:matrix.org> For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there. For WASM shared objects to be remotely usable from JS, you need to be able to interact with them _somehow_. If they are opaque, that only means that interactions must go through a wrapper/Proxy, as littledan said, and also likely need to be valid `WeakMap` keys so that such proxies work. As a result, opaque WASM shared objects are not inherently safer, just slower due to indirection and FFI marshaling. When I brought up having WASM shared objects be opaque entities in a prior structs meeting, the main purpose was to discuss a _worst case_ fallback position if we don't have a comprehensive story for JS shared objects. If WASM shared objects were to be introduced as ordinary JS objects and we were to want to later introduce JS shared structs with unique semantics around field reads and writes (such as what I discussed above re `unsafe`), then mutable WASM shared objects couldn't align with that approach without breaking existing consumers. Whether WASM shared objects are opaque or not has nothing to do with thread safety, only runtime semantic consistency. Thread safety is still a split responsibility between the shared object implementer and shared object consumer based on the needs of any given use case. [07:08:05.0471] > <@mhofman:matrix.org> For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there. attaching prototypes is coming eventually, it's just not prioritized ahead of shared wasmgc [07:09:12.0668] > <@mhofman:matrix.org> what do you mean by making inaccessible? I doubt you mean preventing interaction with instances of share structs outside these blocks? I don't see how could even work. i do mean that. like, imagine all the vtable methods like [[GetOwnProperty]] throw if you're not inside one of these blocks [07:09:28.0478] i said hypothetical [07:09:30.0908] suspend your disbelief [07:09:37.0651] if it's possible, is that considered "enough friction" [07:13:30.0829] here's what i want to do: i'd like to get your side to articulate a greatest lower bound on what's "enough friction", then we analyze why that's considered enough. if there's a design principle there that's not "because we feel like it is", then happy to continue the discussion, otherwise not productive [07:17:22.0973] > <@mhofman:matrix.org> For wasm shared objects, one approach is for them to be opaque obects in JS, without any fields. Afaik, there is also no proposal for attaching prototypes to non-shared wasm refs either. So I fail to see how the complexity for this JS feature would already be there. anyway the real answer is that if the prototype semantics as proposed here isn't part of this proposal, it'll be done as part of the wasm/js API because we still believe that's the best semantics to bridge the shared/unshared worlds [07:28:14.0759] If you wanted, for example, to implement something akin to Rust's `Mutex`, you could do so with a `Proxy` whether it's an opaque WASM shared object or a JS native shared struct. Assuming we could have methods and private state in a JS struct, you could accomplish something similar to [this example](https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads) in the Rust docs (NOTE: uses module expressions): ```js // main.js import { ThreadState } from "./thread_state.js"; import { MutexValue } from "./mutex_value.js"; import { Worker } from "node:worker_threads"; function main() { const counter = new MutexValue(0); const handles = []; for (let i = 0; i < 10; i++) { const handle = new Thread(module { import "./mutex_value.js"; // correlates prototype for MutexValue export function threadStart(counter) { using lck = counter.lock(); const num = lck.unwrap(); num.value += 1; } }, counter); handles.push(handle); } for (const handle of handles) { handle.join(); } using lck = counter.lock(); const num = lck.unwrap(); console.log(`Result: ${num.value}`); } // thread.js shared struct ThreadState { #mut = new Mutex(); #cv = new Condition(); #exited = false; exit() unsafe { using void = new UniqueLock(this.#mut); this.#exited = true; this.#cv.notify(); } join() unsafe { if (this.#exited) return; using lck = new UniqueLock(this.#mut); this.#cv.wait(lck, () => this.#exited); } } export class Thread { #state; #worker; constructor(body, threadData) { this.#state = new ThreadState(); this.#worker = new Worker(module { import "./thread.js"; // correlates prototype for ThreadState import { workerData } from "node:worker_threads"; import { threadStart } from body; const [threadState, threadData] = workerData; try { threadStart(threadData); } finally { threadState.exit(); } }, { workerData: [this.#state, threadData] }); } join() { this.#state.join(); } } // mutex_value.js export shared struct MutexValue { static #Lock = class { #stack; #ref; constructor(owner) unsafe { using stack = new DisposableStack(); stack.use(new UniqueLock(owner.#mutex)); const { proxy, revoke } = Proxy.revocable({ get value() { return owner.#value; }, set value(v) { owner.#value = v; }, }); stack.defer(revoke); this.#ref = proxy; this.#stack = stack.move(); } unwrap() { if (this.#stack.disposed) throw new ReferenceError(); return this.#ref; } [Symbol.dispose]() { using _ = this.#stack; } }; #mutex = new Mutex(); #value; constructor(value) { this.#value = value; } lock() unsafe { return new MutexValue.#Lock(this); } } ``` [07:29:33.0760] * If you wanted, for example, to implement something akin to Rust's `Mutex`, you could do so with a `Proxy` whether it's an opaque WASM shared object or a JS native shared struct. Assuming we could have methods and private state in a JS struct, you could accomplish something similar to [this example](https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads) in the Rust docs (NOTE: uses module expressions): ```js // main.js import { Thread } from "./thread.js"; import { MutexValue } from "./mutex_value.js"; function main() { const counter = new MutexValue(0); const handles = []; for (let i = 0; i < 10; i++) { const handle = new Thread(module { import "./mutex_value.js"; // correlates prototype for MutexValue export function threadStart(counter) { using lck = counter.lock(); const num = lck.unwrap(); num.value += 1; } }, counter); handles.push(handle); } for (const handle of handles) { handle.join(); } using lck = counter.lock(); const num = lck.unwrap(); console.log(`Result: ${num.value}`); } // thread.js shared struct ThreadState { #mut = new Mutex(); #cv = new Condition(); #exited = false; exit() unsafe { using void = new UniqueLock(this.#mut); this.#exited = true; this.#cv.notify(); } join() unsafe { if (this.#exited) return; using lck = new UniqueLock(this.#mut); this.#cv.wait(lck, () => this.#exited); } } export class Thread { #state; #worker; constructor(body, threadData) { this.#state = new ThreadState(); this.#worker = new Worker(module { import "./thread.js"; // correlates prototype for ThreadState import { workerData } from "node:worker_threads"; import { threadStart } from body; const [threadState, threadData] = workerData; try { threadStart(threadData); } finally { threadState.exit(); } }, { workerData: [this.#state, threadData] }); } join() { this.#state.join(); } } // mutex_value.js export shared struct MutexValue { static #Lock = class { #stack; #ref; constructor(owner) unsafe { using stack = new DisposableStack(); stack.use(new UniqueLock(owner.#mutex)); const { proxy, revoke } = Proxy.revocable({ get value() { return owner.#value; }, set value(v) { owner.#value = v; }, }); stack.defer(revoke); this.#ref = proxy; this.#stack = stack.move(); } unwrap() { if (this.#stack.disposed) throw new ReferenceError(); return this.#ref; } [Symbol.dispose]() { using _ = this.#stack; } }; #mutex = new Mutex(); #value; constructor(value) { this.#value = value; } lock() unsafe { return new MutexValue.#Lock(this); } } ``` [07:29:55.0528] * If you wanted, for example, to implement something akin to Rust's `Mutex`, you could do so with a `Proxy` whether it's an opaque WASM shared object or a JS native shared struct. Assuming we could have methods and private state in a JS struct, you could accomplish something similar to [this example](https://doc.rust-lang.org/book/ch16-03-shared-state.html#sharing-a-mutext-between-multiple-threads) in the Rust docs (NOTE: uses module expressions): ```js // main.js import { Thread } from "./thread.js"; import { MutexValue } from "./mutex_value.js"; function main() { const counter = new MutexValue(0); const handles = []; for (let i = 0; i < 10; i++) { const handle = new Thread(module { import "./mutex_value.js"; // correlates prototype for MutexValue export function threadStart(counter) { using lck = counter.lock(); const num = lck.unwrap(); num.value += 1; } }, counter); handles.push(handle); } for (const handle of handles) { handle.join(); } using lck = counter.lock(); const num = lck.unwrap(); console.log(`Result: ${num.value}`); } // thread.js shared struct ThreadState { #mut = new Mutex(); #cv = new Condition(); #exited = false; exit() unsafe { using void = new UniqueLock(this.#mut); this.#exited = true; this.#cv.notify(); } join() unsafe { if (this.#exited) return; using lck = new UniqueLock(this.#mut); this.#cv.wait(lck, () => this.#exited); } } export class Thread { #state; #worker; constructor(body, threadData) { this.#state = new ThreadState(); this.#worker = new Worker(module { import "./thread.js"; // correlates prototype for ThreadState import { workerData } from "node:worker_threads"; import { threadStart } from body; const [threadState, threadData] = workerData; try { threadStart(threadData); } finally { threadState.exit(); } }, { workerData: [this.#state, threadData] }); } join() { this.#state.join(); } } // mutex_value.js export shared struct MutexValue { static #Lock = class { #stack; #ref; constructor(owner) unsafe { using stack = new DisposableStack(); stack.use(new UniqueLock(owner.#mutex)); const { proxy, revoke } = Proxy.revocable({ get value() { return owner.#value; }, set value(v) { owner.#value = v; }, }); stack.defer(revoke); this.#ref = proxy; this.#stack = stack.move(); } unwrap() { if (this.#stack.disposed) throw new ReferenceError(); return this.#ref; } [Symbol.dispose]() { using _ = this.#stack; } }; #mutex = new Mutex(); #value; constructor(value) { this.#value = value; } lock() unsafe { return new MutexValue.#Lock(this); } } ``` [10:14:11.0443] It's quite frustrating and feels counterproductive that, to pursue features like this, we have to resort to these frequent "threats" that it will come anyway. I wish we could focus on how the design should go, rather than whether it should be cancelled. [10:16:08.0188] This style of discourse is a barrier to inclusion. E.g., for AsyncContext, it took years until people joined Chengzhong to talk through why it wasn't a fatally bad idea with respect to SES ideals. [10:17:34.0403] Many us are spending work time on these projects, and this sort of opposition also makes it more difficult to justify spending time on these investments. [10:18:58.0163] Of course we shouldn't add everything to JS, but somehow we need to be able to open the discussion, talk it through, and draw a conclusion, rather than rehashing the same concerns for years.