How to safely access parameters from Task (Porting from Unity IJob)
I am making a program for generating a 3D mesh from parameters.
The generation is done in some Tasks run with
Task.Run
. I'm struggling to grasp how to properly and safely access the parameters when generating.
I am porting the system over from Unity's IJob system, which I think is sort of 'boxing' my thinking in making it hard to get my head around what I need to do.
Do I make a class/struct that has all the parameters in it that I populate beforehand and use those values during generation? Sort of like Unity's Job system.
Thanks 🙂27 Replies
Not necessarily, the way Unity Job system does it like that is for dependency tracking and memory safety, so if that's not a concern you are free to not follow the same design.
Yeah I figured that is the case. But I am not sure what is the 'right' way then since the values can change from the UI during generation. Accessing them directly would be a no go, right?
This for example is fine:
And under the hood, the lambda compiles to a class that captures the
result
variable, so essentially it does what you said about "wrapping data in a class" for you.Well lets say I have something like this
The values in
Foo
could be changed while the task is running which would be not great, right? Or am I misunderstanding something?Yep that is indeed possible and is something you have to deal with yourself. Unity's Job system prevents you from doing this with a lot of safety checks.
Right, that is the question I was asking.
How do I do that safely
Jobs gets around it by only allowing structs, and putting it all in the struct doing the executing. Which is what I was thinking of doing (or making a clone of
SomeClass
and passing it)One potential way is to rewrite it into:
This will guarantee that even if
Foo
and Bar
changes during the execution, it will not affect the running task because it's operating on a copy of it.
However, do keep in mind that foo
and Foo
are still possible to be pointing to the same instance and thus can still be externally modified.That feels weird to do to me. I mean, makes sense, but feels weird. And I guess
Foo
would need to be cloned or something to be safe.Yeah Unity Job system gets around it because it only allow structs and disallows reference types, so structs are copied by value and no reference type = no possibility of external modification. Native containers are still pointers and basically share the same problems as reference types, so Unity has a bunch of safety checks in place.
If you are simply worrying about UI changing causing ongoing tasks to get into race conditions, one simple solution could just be disable UI until the task is done. It depends on if you think it's worth it to care about user changing UI while the task is already ongoing.
I guess that is possible, but feels like worse UX. Will probably just go the Unity Jobs approach and limit it to structs, or clones in some cases.
I guess I could forgo the '
IJob
' structs and just capture local variables as you showed before. Would you say that capturing the variables is the more 'C# standard' way?Sure, making a struct and constructing it with all the values you need, is basically just a more verbose capture.
Another way you can do it is with records, because they are immutable by default you don't need to worry those things, and C# has syntax sugars to help working with records.
Oh it does?
Yep, record member have init only setters, so immutable after construction. If all your members are also records, then they don't have reference type mutation issues either.
Huh, didn't realize they were immutable. Interesting!
Yep, helps make code very simple too.
Ooh yeah I totally forgot you could define records like that!
What would be the way you would recommend doing it? Variable capture? Record? Struct?
Not sure tbh, but if you are worrying about potential race conditions then records is the simplest way to ensure they can't happen (you would still need to deal with cancelling ongoing tasks though, using cancellation token), and the code probably looks pretty similar to how you write jobs but cleaner.
Alrighty, well this definitely gives me clarity on it all. Thank you for the help and insight as always! 😄
im not really sure if this sufficient like this.
first of all, unity brings its own synchronization context, which executes task on the game loop.
unless u slap a
.ConfigureAwait(false)
onto the task returned by Task.Run()
u will might run into fps drops if this still runs in unity (basically same issue for GUI frameworks WinForms, WPF, AvaloniaUI and maybe also MAUI - its not clear to me if this is in still Unity or not)
Generate()
sounds to me like it does some quite CPU heavy stuff, which might take some seconds, then these should created with TaskCreationOptions.LongRunning
. im not sure what unity's synchronization context does here, but the default/none will be handled by running this on a dedicated thread.
that would mean instead of Task.Run(´lambda)
u would call Task.Factory.StartNew(lambda, TaskCreationOptions.LongRunning);
a second is already long running. and that is about the synchronous code (basically each code section between await
s) in ur task.
here is also some general info: https://devblogs.microsoft.com/pfxteam/task-run-vs-task-factory-startnew/Stephen Toub - MSFT
.NET Parallel Programming
Task.Run vs Task.Factory.StartNew
In .NET 4, Task.Factory.StartNew was the primary method for scheduling a new task. Many overloads provided for a highly configurable mechanism, enabling setting options, passing in arbitrary state, enabling cancellation, and even controlling scheduling behaviors. The flip side of all of this power is complexity.
You might be missing a bit of the context, OP's question is "I'm familiar with Unity's Job system, what's the idiomatic way to do the same in a plain C# app?"
Appreciate the info! And yeah, as Burrito said, this is not in Unity, just familiar with the Unity Jobs system and way of doing things. And trying to understand how you would do the same in a plain C# app.
thats why i mentioned the GUI frameworks as well, and that would generally only be relevant for the
.ConfigureAwait()
, the rest still applies even for the default/no synchronization context which would then use the thread poolTIL about long running.
I've been spoiled by Unity's Job system and
UniTask.RunOnThreadPool
that I didn't know about this.Pretty sure that ConfigureAwait is ignored in Unity
And that continuations are always run on the main thread
didnt know that, thx for info
It also causes
.GetAwaiter().GetResult()
to always deadlock when called from the main thread Ahh I see, thank you. I will keep
.ConfigureAwait()
in mind, wasn't aware of it!
I was aware of LongRunning, but I don't think it applies in this case as I want the the generation to happen in 'real time' (less than ~100ms) as the user changes properties in the UI.
So if it is taking longer than that, I need to do some more optimization haha.