Quick answer: Godot 4 C# ResourceLoader.LoadThreadedRequest callback running on the worker thread? GDScript ResourceLoader handles thread marshalling; C# requires CallDeferred to reach main.

Async-loaded texture assigned to a TextureRect on the worker thread. Godot asserts because Node mutations must be main-thread.

Marshal via CallDeferred

this.CallDeferred(MethodName.OnLoaded, resource);

Defers the callback to the main thread. Required for any Node-touching code.

Poll on main thread

Instead of a callback, poll LoadThreadedGetStatus in _Process. Result is delivered on main; no marshalling.

Use SignalAwaiter

For C# idiomatic code, await a signal that's emitted from a CallDeferred. Code reads as single-threaded; runtime is correctly threaded.

“GDScript hides threading bookkeeping. C# requires you to do it.”

Wrap async load in a single helper that returns a Task. Internal CallDeferred is invisible to the caller; threading concerns stop spreading.