Skip to main content

Features

Complete feature reference for Valkarn Tasks.


Core async primitive​

ValkarnTask struct​

A zero-allocation, struct-based async return type that replaces both UniTask and Unity's Awaitable.

async ValkarnTask LoadLevel() { ... }
async ValkarnTask<int> CountEnemies() { ... }
  • Zero-allocation synchronous fast path — if the method completes without ever suspending, zero heap allocations occur.
  • Pooled async path — if the method suspends, the state machine runner is taken from a bounded, shrinkable pool. Zero boxing, bounded pools with automatic trimming.
  • IL2CPP-first pooling — pool operations on the main thread use zero atomics. IL2CPP penalizes Volatile by 9.2× and Interlocked by 2.9× vs Mono; Valkarn avoids both on the hot path.
  • Generational token safety — a uint generation counter per pool slot. 4,294,967,296 cycles per slot before collision — impossible in practice. (UniTask uses a short token: collision after ~18 minutes of active async work.)

Result<T> — exception-free error handling​

var result = await loadTask.AsResult();

if (result.Succeeded) Use(result.Value);
else if (result.IsCanceled) ShowRetry();
else Debug.LogError(result.Error);

Result<T> and Result are readonly struct values that represent the outcome of a task without throwing. Both support implicit conversion to bool (true when succeeded).

Manual completion sources​

var promise = new ValkarnTask.Promise<string>();
promise.TrySetResult("done");
string value = await promise.Task;

Supports TrySetResult, TrySetException, and TrySetCanceled. Finalizer-based unobserved exception reporting ensures errors are never silently lost.

Pooled completion sources​

Auto-reset pooled variants for repeating patterns (used internally by channels and combinators):

var source = ValkarnTask.PooledPromise<int>.Create(out uint token);
source.TrySetResult(42);
int value = await source.Task; // source returns to pool automatically

Awaitable bridge​

Transparent interop with Unity's Awaitable — no manual conversion:

async ValkarnTask LoadGame()
{
await SceneManager.LoadSceneAsync("Level2"); // Unity Awaitable — works directly
await Resources.LoadAsync<Texture2D>("hero"); // Unity Awaitable — works directly
await ValkarnTask.Delay(1000); // Valkarn native
}

The source generator detects Awaitable awaits and generates the adapter automatically.


Lifecycle cancellation​

Automatic (source-generated)​

Mark the class partial — the source generator does the rest:

public partial class EnemySpawner : MonoBehaviour
{
async ValkarnTask Start()
{
while (true)
{
await ValkarnTask.Delay(3000);
SpawnWave();
// Automatically cancelled when this GameObject is destroyed.
}
}
}
  • MonoBehaviour — bound to destroyCancellationToken
  • ScriptableObject — bound to application lifetime
  • Plain class — no automatic binding (manual token required)

Opt-out​

[NoAutoCancel]
async ValkarnTask BackgroundSync()
{
await SyncToServer(); // not auto-cancelled on destroy
}

Manual CancellationToken override​

Passing an explicit CancellationToken overrides the auto-injected lifecycle token:

async ValkarnTask DoWork(CancellationToken ct)
{
await ValkarnTask.Delay(1000, ct);
}

No sibling cancellation​

Tasks never cancel sibling tasks. WhenAll waits for ALL tasks; WhenAny returns the first result but losing tasks continue running. This prevents data corruption when tasks have side effects.


Critical sections​

For operations that must not be interrupted by lifecycle cancellation:

async ValkarnTask SaveProgress()
{
var data = await LoadPlayerData(); // cancellable

await using (ValkarnTask.Critical())
{
await db.Insert(data); // NOT cancelled even if GO is destroyed
await db.Commit();
} // pending cancellation applies here

await SendNotification(); // cancellable again
}

Inside a critical section, cancellation is deferred — not ignored. When the section ends, pending cancellation is applied.


Combinators​

WhenAll (typed)​

// Direct — throws if any task fails
var (enemies, map) = await ValkarnTask.WhenAll(LoadEnemies(), LoadMap());

// Safe — wrap with AsResult() for non-throwing behavior
var (a, b) = await ValkarnTask.WhenAll(
LoadEnemies().AsResult(), LoadMap().AsResult());

Zero-allocation sync fast path when all tasks are already completed. IEnumerable<ValkarnTask<T>> overload uses ArrayPool<T> for internal arrays.

WhenAll (void)​

await ValkarnTask.WhenAll(SaveA(), SaveB(), SaveC());
await ValkarnTask.WhenAll(taskList); // IEnumerable<ValkarnTask>

WhenAny​

var (winnerIndex, result) = await ValkarnTask.WhenAny(DownloadFromA(), DownloadFromB());

Returns the first completed result. Losing tasks continue running naturally.

Fire-and-forget​

SendAnalytics("event").Forget();

[FireAndForget]
async ValkarnTask SendAnalytics(string eventName) { ... }
// Callers need no .Forget() — no warning generated

.Forget() routes errors to ValkarnTask.UnobservedException. Never silently swallowed.

AsNonGeneric​

ValkarnTask voidTask = typedTask.AsNonGeneric();

Time and delay​

await ValkarnTask.Delay(1000);                                    // milliseconds
await ValkarnTask.Delay(TimeSpan.FromSeconds(2)); // TimeSpan
await ValkarnTask.Delay(1000, DelayType.UnscaledDeltaTime); // ignore timescale
await ValkarnTask.Delay(1000, DelayType.Realtime); // Stopwatch-based

await ValkarnTask.Yield(); // next PlayerLoop tick
await ValkarnTask.Yield(PlayerLoopTiming.FixedUpdate); // specific timing
await ValkarnTask.NextFrame(); // guaranteed next frame
await ValkarnTask.DelayFrame(5); // N frames

await ValkarnTask.WaitUntil(() => player.IsReady);
await ValkarnTask.WaitWhile(() => isLoading);

Thread switching​

async ValkarnTask ProcessData()
{
var raw = await DownloadData();

await ValkarnTask.SwitchToThreadPool();
var processed = HeavyComputation(raw); // background thread

await ValkarnTask.SwitchToMainThread();
ApplyToGameObject(processed); // main thread
}

16 PlayerLoop timings​

GroupTimings
InitializationInitialization, LastInitialization
EarlyUpdateEarlyUpdate, LastEarlyUpdate
FixedUpdateFixedUpdate, LastFixedUpdate
PreUpdatePreUpdate, LastPreUpdate
UpdateUpdate, LastUpdate
PreLateUpdatePreLateUpdate, LastPreLateUpdate
PostLateUpdatePostLateUpdate, LastPostLateUpdate
TimeUpdateTimeUpdate, LastTimeUpdate

All operations default to PlayerLoopTiming.Update unless specified otherwise.


Channels​

// Unbounded
var channel = ValkarnTask.Channel.CreateUnbounded<EnemySpawnRequest>();

// Bounded — backpressure when full
var channel = ValkarnTask.Channel.CreateBounded<LogEntry>(capacity: 100);

// Multi-consumer
var channel = ValkarnTask.Channel.CreateUnbounded<WorkItem>(multiConsumer: true);

// Producer
await channel.Writer.WriteAsync(entry);
bool accepted = channel.Writer.TryWrite(entry);

// Consumer
await foreach (var item in channel.Reader.ReadAllAsync())
Process(item);

// Completion
channel.Writer.Complete();
await channel.Reader.Completion;

Deterministic testing (TestClock)​

[Test]
public void Respawn_WaitsThreeSeconds()
{
var clock = new TestClock();
var task = spawner.RespawnEnemy();

clock.Advance(TimeSpan.FromSeconds(2));
Assert.IsFalse(task.IsCompleted);

clock.Advance(TimeSpan.FromSeconds(1));
Assert.IsTrue(task.IsCompleted);
}

All time-dependent operations read from TimeProvider.Current. In tests, replace it with TestClock. AdvanceFrame() simulates a single player loop tick.


Job System bridge​

var job = new PathfindingJob { start = a, end = b, results = results };
await job.ScheduleAsync();
// results NativeArray is readable immediately after await

await job.ScheduleParallelAsync(dataCount, batchSize); // IJobParallelFor

// Cancellation — complete the job handle before reporting cancellation (no job leak)
await job.ScheduleAsync(cancellationToken);

Compile-time diagnostics​

CodeSeverityDescription
TT001WarningDouble-await / use-after-free on a ValkarnTask
TT002Errorasync ValkarnTask result used as expression statement — must be awaited or .Forget()
TT010InfoAsync method in MonoBehaviour auto-cancelled on Destroy
TT011WarningWhenAll contains tasks with different lifetimes
TT012WarningAsync loop without cancellation check (potential zombie loop)
TT013WarningValkarnTask returned but never awaited and not explicitly discarded
TT014Warning[NoAutoCancel] without manual CancellationToken parameter
TT015InfoAwaiting Awaitable inside ValkarnTask — bridge adapter generated
TT016WarningAsync method with no await expression
TT017Warning[FireAndForget] on ValkarnTask<T> — discards return value

Pool management​

Every async method runner is pooled via ValkarnPool<T>:

  • Main thread — lock-free stack, zero atomics
  • Background threads — Treiber lock-free stack with CAS operations
  • Frame-based trimming — every 300 frames (~5s at 60fps), excess objects are released gradually
  • Never shrinks below configurable minimum (default: 8)

Monitor at runtime:

foreach (var (type, size, maxSize) in ValkarnTask.GetPoolInfo())
Debug.Log($"{type}: {size}/{maxSize}");

ValkarnTaskSettings​

Configure via ScriptableObject (Assets > Create > Valkarn > Tasks > Task Settings, place in Resources/):

SettingDefaultDescription
DefaultMaxPoolSize256Max items per pool type
MinPoolSize8Never trim below this
TrimCheckInterval300Frames between trim checks
TrimHysteresisCount2Consecutive checks before trimming
TrimReleaseRatio0.25Fraction of excess released per cycle
EnableAutoCanceltrueAuto-bind MonoBehaviour tasks to destroyCancellationToken
LogUnobservedCancellationsfalseLog unobserved cancellations as warnings
MaxExceptionLogsPerFrame10Cap exception logs per frame

Error handling​

// Unobserved exceptions — fired deterministically at pool return
ValkarnTask.UnobservedException += ex =>
Debug.LogError($"Unobserved: {ex}");
  • WhenAll — throws first exception, routes additional ones to UnobservedException
  • WhenAny — winner's exception is thrown; loser faults go to UnobservedException
  • Lifecycle cancellation — OperationCanceledException suppressed by default (configurable)

Factory methods​

ValkarnTask.CompletedTask          // void, zero-alloc
ValkarnTask.FromResult<T>(value) // typed, zero-alloc
ValkarnTask.FromException(ex) // faulted
ValkarnTask.FromException<T>(ex)
ValkarnTask.FromCanceled() // canceled
ValkarnTask.FromCanceled<T>()
ValkarnTask.Never // never completes (sentinel for WhenAny)