Sharing Data Between Workers
Compiled programs can use node:worker_threads to run work in parallel on several CPU cores. As in Node.js, each message a worker receives is a copy (a structured clone) of what was sent, so a large data set sent to four workers is copied four times. See worker threads for the supported worker APIs.
When workers need to read the same large data, use the @scriptc/threads package. Its publish(value) function makes a graph of objects immutable, so a scriptc executable can hand the same objects to every thread by reference instead of copying them.
Add @scriptc/threads to your project's dependencies. scriptc compiles its functions directly into your program, and Node.js runs its JavaScript implementation, so the same program runs under both.
Example
The main thread builds a ledger of a million orders, publishes it, and sends it to four workers. Each worker sums its share of the orders.
Node.js 24 runs the program directly and clones the ledger for each worker:
The scriptc executable prints the same results, except that every worker reads the main thread's ledger in place:
The publication contract
publish(value) returns value after making it, and everything it reaches, immutable. Publishing an already published graph returns immediately.
| Values | publish |
|---|---|
Numbers, strings, booleans, BigInts, undefined, null | Published |
| Plain objects, arrays, class instances, records | Published, with their fields and elements |
Map and Set | Published, with their keys and values |
Functions, symbols, accessor properties, Date, RegExp, Error, Promise, typed arrays, other built-in objects | Refused with a TypeError |
A refusal names the value, for example Cannot publish a function or Cannot publish a Date object. Only values that are actually present count: a class whose field type could hold an unpublishable value publishes as long as no such value is reached. A refused publish leaves the whole graph unchanged and writable, because both implementations check the graph before they change anything.
The write guard
Writes into a published graph throw the TypeError that Node.js throws for frozen objects, in both implementations:
- Field and property assignment:
Cannot assign to read only property 'title' of object '#<Meeting>'. - Adding array elements:
Cannot add property 3, object is not extensible. - Removing array elements, for example with
pop():Cannot delete property '2' of [object Array]. - Changing an array's length:
Cannot assign to read only property 'length' of object '[object Array]'. set,add,delete, andclearon a publishedMaporSet:Cannot modify a published Map(orSet).
In a scriptc executable, every store the compiler emits into a type that the program publishes checks whether its target is published. Objects of those types that were never published stay writable.
How it differs from structured clone
Node.js keeps cloning published graphs when it posts them; sharesPublishedGraphs is false there. A scriptc executable passes a published graph to postMessage and workerData as a pointer, and sharesPublishedGraphs is true.
| Behavior | scriptc executable | Node.js |
|---|---|---|
| Sending a published graph | Constant time; the receiver reads the sender's objects | A structured clone per message |
| Received graph | Immutable | A writable clone, until the receiver publishes it |
| Class instances | Keep their class, methods, and instanceof relations | Arrive as plain objects without their prototype |
| Identity across messages | Two posts of one graph arrive as the same objects | Every post arrives as a new clone |
| Memory | Published objects are never freed | Garbage collected as usual |
A program behaves the same under both when it follows three rules:
- A receiver calls
publishon what it receives. In scriptc this is a no-op; in Node.js it freezes the clone, so writes fail in the same way. - A receiver reads data and does not depend on prototypes, methods, or
instanceof. Error messages that name the receiver's class differ accordingly:#<Meeting>in scriptc,#<Object>in Node.js. - Nothing compares identity across messages.
Frozen plain data
Without @scriptc/threads, a scriptc executable also shares a message of untyped values, such as JSON.parse results, by reference when every object and array in it is frozen with Object.freeze, ordinary, and holds only primitives, strings, and other such objects and arrays. That data becomes immortal in the same way. Any other message is cloned. Set SCRIPTC_PUBLISH=0 in the environment to clone every message.
Cost
Starting a worker thread takes tens of microseconds plus the worker's own module initialization, since every worker initializes the program's modules again, as in Node.js. A worker that exits frees its heap first, which takes time proportional to what it still holds. SCRIPTC_WORKER_STATS=1 prints, as each worker exits, how long it took to start, to run its script and to exit.
On Linux, a worker that exits without having shared an object it allocated with another thread returns all of its memory, including objects it never freed, such as ProgramLifetime instances, once no other worker thread is running. A worker shares objects when, for example, it sends a published value or frozen plain data, which other threads read by reference, or stores a value in an InternTable. The memory of a worker that shared stays allocated, as on other platforms. Set SCR_SA_DISCARD=0 in the environment to keep the memory of every worker.
publish visits each object of the graph once and marks it immortal, so publication takes time proportional to the graph. Published objects then cost nothing on later sends, and reference counting and the cycle collector skip them. Stores into the types a program publishes carry one extra load and branch.
Threads that add to one shared collection of published values while they run can use an InternTable from @scriptc/experimental.
See published graphs for remaining differences from Node.js.