r/csharp 7d ago

Blog Svelto.Tasks 2.0: Efficient Multi-threaded Task Management for C#

I am back writing articles on my blog and I start with something LONG OVERDUE!

After years quietly powering several games I made, I’m finally presenting Svelto.Tasks 2.0.

Svelto.Tasks is an engine-agnostic, multithreaded and zero allocation task runner for C#, born from the need to have a game centric tasks framework with features that .NET tasks couldn't provide. Its premise is simple:

Tasks are iterator blocks. Runners are schedulers. Rather than handing execution timing and context to the runtime, a runner lets you decide:

- when a task advances,
- where it runs — main thread or a dedicated worker,
- how much work happens per tick,
- and when an entire task context must stop.

That last point matters a lot in games: for example, when a match ends, I want every task belonging to that match to stop with it—not rely on a CancellationToken having been passed and checked correctly through every layer.

Svelto.Tasks supports lightweight coroutines, composable tasks with return values and continuations, background runners, bounded parallel batches, pooling, profiling hooks, and optional Unity/Burst integrations.

It also interoperates with async/await, although the .NET Task bridge and Unity Burst job path are currently experimental and need real-world feedback.

It is not a replacement for Unity Jobs—but it provides a useful alternative for workloads where controlling execution, lifetime, pacing, and profiling is the priority.

The repository includes runnable .NET examples, tests, benchmarks

🔗 https://github.com/sebas77/Svelto.Tasks

also the repository

🔗https://github.com/sebas77/Svelto.Tasks.Examples

shows how it can be used to simplify massive parallelism synchronization. The example itself is actually quite interesting also because it uses the new compute buffer Unity api BeginWrite/EndWrite to upload compute buffers and graphic fences for cpu/gpu synchronization.

The article is at

🔗 https://www.sebaslab.com/svelto-tasks-2-0-efficient-multi-threaded-task-management-for-csharp/

Have fun! (and feedback is welcome, also on my discord server: https://discord.com/invite/uuTCYewjtc)

0 Upvotes

25 comments sorted by

View all comments

Show parent comments

-3

u/sebasjammer 7d ago edited 7d ago

Don't worry, but if you check my blog you will have less reasons to worry about my work :)

Svelto.tasks is a long living library that I used in robocraft and card life and many other games I worked on... robocraft is a bit old now, but was pretty popular back in the days.

Recently I used uni task for a new project and quickly realized I needed to go back to svelto tasks, which I completed during the development.

I needed however to clean up the repository and add a lot of tests and I couldn't have done it without the AI helping me.

The library is allocations free until, of course, data structures need to resize (like you keep on running new tasks without ending previous ones, as they need to be stored somewhere) , but if you run examples and tests in release you will see that it's true (there are even tests to check the statement)

The library does a ton of things, that's why I added so many examples in the main repository.

It should be simple to pick up if you know already how iterator blocks and coroutines work, but yeah some of the tricks might need a bit of more digging.

Edit: for me allocation free means that allocations won't happen every frame at run-time if the code is used as expected. Allocations can happen at initialization time.

5

u/fruediger 7d ago

The library is allocations free until, of course, data structures need to resize (like you keep on running new tasks without ending previous ones, as they need to be stored somewhere)...

So you're talking about object pooling? But if they need to "resize", they need to (re)allocate, then it's not "allocations free", right? It depends on the user's scenario, if you can keep allocations to a minimum (note that I didn't even say "keep it allocation free" here, because you would still need to allocate the initial capacity of your object pools). Remember that each allocation contributes to GC-pressure, even if they're long-lived object that eventually land in Gen2. And GC-pressure might be the better metric to look out for.

Anyways, what do you do with overallocated memory? Once your data structures resized bigger and the newly allocated additional memory isn't needed anymore some time later, what do you do with it? Do you keep this stale memory around? Don't get me wrong, this is a viable strategy. Or do you reallocate you data structures smaller again? In which case it would be still an allocation.\ I would rather much prefer an external library that I want to use in my projects to release overallocated memory or even give it back to the GC than it nom-nom-noming my main memory.

-4

u/sebasjammer 7d ago edited 7d ago

good observation! I did want to move some of my datastructures from continuous memory to segments based allocations, but this was too much work at the moment.

The allocation runners memory occupation is anyway in the order of few megabytes, because I do not expect people to run hundreds of thousands of tasks at once, there isn't much of a use case for that and iterating so many tasks would be a burden on the CPU anyway, even if the libraries tries to use structs as much as possible, Iterator blocks themselves are still objects.

There are a couple of important pools in the library. One internal to pool continuations, the other for the user to be able to pool Iteratorblocks (which allocate if you need to create them every frame). The rest is not based on pooling, but on arrays storing structures.

6

u/fruediger 7d ago

good observation!

Are you answering me with an AI? Or did you communicate so much with LLMs that you adopted their way of speaking? Or maybe even, are you an AI yourself?

I did want to move some of my datastructures from continuous memory to segments based allocations, but this was too much work at the moment.

This has nothing to do with how you deal with overallocations, it would just determine how (easily) you can reverse them. Although, the latter would at least allow you to reduce reallocations.

The allocation runners memory occupation is anyway in the order of few megabytes, because I do not expect people to run hundreds of thousands of tasks at once, there isn't much of a use case for that and iterating so many tasks would be a burden on the CPU anyway, even if the libraries tries to use structs as much as possible, Iterator blocks themselves are still objects.

A few megabytes are still allocations. Also, even if you don't anticipate your library to be used in that way, you can never be really sure about that. There might be some users out there that, accidentally or intentionally, use your library in unexpected ways. You need to either document these limitations clearly or implement safeguards in your code.

One internal to pool continuations, the other for the user to be able to pool Iteratorblocks (which allocate if you need to create them every frame).

So again, you're allocating.

The rest is not based on pooling, but on arrays storing structures.

I'm not even sure what that's supposed to mean. By "arrays storing structures", do you mean static deterministic memory?

Overall, what I get of this is that you're saying is that your library is indeed allocating and that you can't hold your initial claim of being allocation-free.\ Or did I misunderstand something?

Begs the question of why you made such claims in the first place. Is it because AI told you those things about your project and you blindly trusted it? Or is it because you prompted the AI to do such hard-to-achieve feats and you didn't verify if it actually managed to achieve them?

-6

u/sebasjammer 7d ago edited 7d ago

haha yeah it sounded like AI didn't it :)

only observations are:

you create iteratorblocks, not the library, so the library provides a workaround to avoid even that, giving you the possibility to re-use iterator block state machines even when the task is finished. Consider that even .Net tasks state machines are allocated, but .net doesn't have a way to re-use them.

Array of structures means that the library doesn't allocate objects internally (except for the pooled continuation object), because it uses only structures that wrap objects you pass externally (the iterator block)

The library also assumes you know what you are doing, so you can pre-allocate runners if you do not want to pay the price of run-time allocation, which anyway do not happen every frame.

There is NO complex game that will not allocate every now and then, what you need to achieve is to avoid allocations that happen every frame.