メインコンテンツまでスキップ

ValkarnTask<T>

ValkarnTask<T>はValkarn Tasksの値を返す非同期タスク型です。readonly structであり、同期的に完了した場合はインライン結果を、非同期完了した場合はプールされたソースオブジェクトへの参照を保持します。

名前空間: UnaPartidaMas.Valkarn.Tasks

[AsyncMethodBuilder(typeof(CompilerServices.AsyncValkarnTaskMethodBuilder<>))]
[StructLayout(LayoutKind.Auto)]
public readonly struct ValkarnTask<T>

Tに対するジェネリック制約はありません。任意の型 — 値型、参照型、構造体、クラス — が有効です。


インスタンスの作成

同期完了タスク

これらのファクトリーメソッドはバッキングソースオブジェクトのないValkarnTask<T>を返します。ゼロアロケーション。

ValkarnTask.FromResult<T>(T value)

valueをインラインに保持した完了済みValkarnTask<T>を返します。非ジェネリックのValkarnTask型の静的メソッドとして宣言されています。

public static ValkarnTask<T> FromResult<T>(T value)
ValkarnTask<int> task = ValkarnTask.FromResult(42);
ValkarnTask<string> name = ValkarnTask.FromResult("Valkarn");
ValkarnTask<Vector3> pos = ValkarnTask.FromResult(transform.position);

返された構造体はsource == nullです。awaitするとコンティニュエーションのアロケーションは発生しません — コンパイラーは即座にIsCompleted == trueを確認します。

ValkarnTask.FromException<T>(Exception exception)

フォルトしたValkarnTask<T>を返します。awaitするとExceptionDispatchInfoによって元のスタックトレースを保持しながら例外が再スローされます。

public static ValkarnTask<T> FromException<T>(Exception exception)
ValkarnTask<Texture2D> LoadTexture(string path)
{
if (string.IsNullOrEmpty(path))
return ValkarnTask.FromException<Texture2D>(
new ArgumentException("パスは空にできません。", nameof(path)));

return LoadTextureAsync(path);
}

ValkarnTask.FromCanceled<T>(CancellationToken ct = default)

キャンセルされたValkarnTask<T>を返します。awaitするとOperationCanceledExceptionがスローされます。

public static ValkarnTask<T> FromCanceled<T>(CancellationToken ct = default)
ValkarnTask<byte[]> Download(string url, CancellationToken ct)
{
if (ct.IsCancellationRequested)
return ValkarnTask.FromCanceled<byte[]>(ct);

return DownloadAsync(url, ct);
}

asyncメソッド経由

ValkarnTask<T>を返すと宣言されたasyncメソッドは自動的にAsyncValkarnTaskMethodBuilder<TResult>を使用します:

async ValkarnTask<int> ComputeAsync()
{
await ValkarnTask.Yield();
return 42;
}

コンパイラーはステートマシンを生成します。メソッドが同期的に完了した(一度もサスペンドしない)場合、AsyncValkarnTaskMethodBuilder<T>.Tasksource == nullnew ValkarnTask<T>(result)を返します — ゼロアロケーション。


スレッドプールでの作業実行

これらのメソッドは.NETスレッドプール上でデリゲートを実行し、指定されたPlayerLoopTimingでメインスレッドに結果を返します。長い名前のRunOnThreadPoolバリアントのラッパーです。

ValkarnTask.Run<T>(Func<T> func, PlayerLoopTiming timing, CancellationToken ct)

スレッドプール上で同期Func<T>を実行します。

public static ValkarnTask<T> Run<T>(
Func<T> func,
PlayerLoopTiming timing = PlayerLoopTiming.Update,
CancellationToken cancellationToken = default)
// スレッドプールで計算し、次のUpdateでメインスレッドに結果が届く
int hash = await ValkarnTask.Run(() => ComputeExpensiveHash(data));

ValkarnTask.Run<T>(Func<ValkarnTask<T>> func, PlayerLoopTiming timing, CancellationToken ct)

スレッドプール上で非同期Func<ValkarnTask<T>>を実行します。作業自体が非同期(例:ファイルI/O)の場合に使用します。

public static ValkarnTask<T> Run<T>(
Func<ValkarnTask<T>> func,
PlayerLoopTiming timing = PlayerLoopTiming.Update,
CancellationToken cancellationToken = default)
string json = await ValkarnTask.Run(async () =>
{
using var reader = File.OpenText("data.json");
return await reader.ReadToEndAsync();
});

両方のRunオーバーロードはトークンが既にキャンセルされている場合は早期キャンセルし、作業完了後にtimingでメインスレッドに切り替えます。


インスタンスメンバー

IsCompleted

public bool IsCompleted { get; }

タスクが任意の終端状態(Succeeded、Faulted、またはCanceled)で完了した場合にtrueを返します。同期完了タスク(source == null)では、インターフェースディスパッチなしで常にtrueを返します。

var task = SomeLongOperation();
if (task.IsCompleted)
{
int result = task.GetAwaiter().GetResult();
Use(result);
}

GetStatus()

public ValkarnTask.Status GetStatus()

現在のValkarnTask.Statusを返します。取りうる値:PendingSucceededFaultedCanceledsource == nullの場合は常にSucceededを返します。

GetAwaiter()

public Awaiter GetAwaiter()

Awaiter構造体を返します。コンパイラーがawaitを実装するために使用します。IsCompletedがtrueの場合のみ安全な同期的な結果取得にも使用できます。

ValkarnTask<int> task = ValkarnTask.FromResult(10);
int value = task.GetAwaiter().GetResult(); // 安全 — 同期完了済み

保留中のタスクでGetResult()を呼び出すとInvalidOperationExceptionがスローされます。

AsNonGeneric()

public ValkarnTask AsNonGeneric()

このValkarnTask<T>を非ジェネリックのValkarnTaskに変換し、結果の型を破棄します。結果のタスクは同じ基礎ソースとトークンを共有するため、同時に完了します。

ValkarnTask<int> typedTask = ComputeAsync();
ValkarnTask voidTask = typedTask.AsNonGeneric();
await voidTask; // 完了を待つ、結果は無視

これは混合型タスクをコンビネーターに渡す場合や、値ではなく完了タイミングのみに関心がある場合に有用です。


ValkarnTask<T>を返すコンビネーター

WhenAll — 型付き2タスクオーバーロード

public static ValkarnTask<(T1, T2)> WhenAll<T1, T2>(
ValkarnTask<T1> task1, ValkarnTask<T2> task2)

両方のタスクを並行して実行し、結果のタプルを返します。いずれかのタスクがフォルトまたはキャンセルされた場合、最初の例外が優先され、もう一方のエラーはValkarnTask.UnobservedExceptionを通じて報告されます。

タプル分解はC#の分解代入で自然に機能します:

var (profile, inventory) = await ValkarnTask.WhenAll(
FetchProfileAsync(userId),
FetchInventoryAsync(userId)
);

ゼロアロケーション高速パス: 両方のタスクが呼び出し地点で同期的に完了している場合、プールされたオブジェクトは作成されず、結果のタプルはインラインで返されます。

WhenAll — 型付きコレクションオーバーロード

public static ValkarnTask<T[]> WhenAll<T>(IEnumerable<ValkarnTask<T>> tasks)

コレクション内のすべてのタスクを並行してawaitし、インデックス順にT[]を返します。

var urls = new[] { "https://a.com", "https://b.com", "https://c.com" };
ValkarnTask<string>[] downloads = urls.Select(u => DownloadAsync(u)).ToArray();
string[] results = await ValkarnTask.WhenAll(downloads);

コレクションが空の場合、ValkarnTask.FromResult(Array.Empty<T>())を返します — ゼロアロケーション。すべてのタスクが既に同期的に完了している場合、コンビネータープロミスを作成せずにインラインで結果配列が構築されます。

WhenAny — 型付き2タスクオーバーロード

public static ValkarnTask<(int winnerIndex, T result)> WhenAny<T>(
ValkarnTask<T> task1, ValkarnTask<T> task2)

いずれかのタスクが完了したら即座に返します。結果のタプルには勝者の0ベースのインデックスとその値が含まれます。負けたタスクは実行を続けます;それらのエラー(あれば)はValkarnTask.UnobservedExceptionを通じて報告されます。負けたキャンセルは意図的に報告されません。

var (winnerIndex, result) = await ValkarnTask.WhenAny(
FetchFromCacheAsync(key),
FetchFromNetworkAsync(key)
);

if (winnerIndex == 0)
Debug.Log("キャッシュが勝ち");

WhenAny — 型付きコレクションオーバーロード

public static ValkarnTask<(int winnerIndex, T result)> WhenAny<T>(
IEnumerable<ValkarnTask<T>> tasks)

2タスクオーバーロードと同じセマンティクスで、任意の数のタスクに拡張されています。少なくとも1つのタスクが必要;空のコレクションに対してはArgumentExceptionがスローされます。

var tasks = servers.Select(s => s.FetchAsync(query)).ToArray();
var (winnerIndex, data) = await ValkarnTask.WhenAny(tasks);
Debug.Log($"サーバー{winnerIndex}が最初に応答");

ファクトリー便利メソッド

ValkarnTask.Create<T>(Func<ValkarnTask<T>> factory)

ファクトリーデリゲートを呼び出し、結果のタスクをawaitします。非同期操作の構築を遅延させたい場合に有用です。

public static async ValkarnTask<T> Create<T>(Func<ValkarnTask<T>> factory)
var result = await ValkarnTask.Create(() => LoadLevelDataAsync(levelId));

Awaiter構造体(ネスト)

ValkarnTask<T>.Awaiterはコンパイラー向けのawaiterです。ICriticalNotifyCompletionを実装するreadonly structです。通常は直接操作しません。

public readonly struct Awaiter : ICriticalNotifyCompletion
{
public bool IsCompleted { get; }
public T GetResult();
public void OnCompleted(Action continuation);
public void UnsafeOnCompleted(Action continuation);
}

UnsafeOnCompletedAsyncValkarnTaskMethodBuilder<T>が使用するパスです。「unsafe」ラベルはExecutionContextがキャプチャされないことを意味します — これはUnityではSynchronizationContextが有効でないため意図的です。

IsCompletedがtrueの場合、GetResult()を呼び出すと、同期タスクの場合はインラインresultフィールドを読み取り、非同期タスクの場合はソースインターフェースを通じて呼び出します。両方のパスは[MethodImpl(MethodImplOptions.AggressiveInlining)]です。


ValkarnTask<T>の拡張メソッド

AsResult<T>()

public static ValkarnTask<Result<T>> AsResult<T>(this ValkarnTask<T> task)

ValkarnTask<T>ValkarnTask<Result<T>>にラップし、例外またはキャンセルをキャッチしてResult<T>値にエンコードします。これにより呼び出し元でのtry/catchが不要になります。

Result<string> result = await FetchDataAsync(url).AsResult();

if (result.IsSuccess)
Process(result.Value);
else if (result.IsCanceled)
Debug.Log("キャンセルされました");
else
Debug.LogError(result.Exception);

同期高速パス: ソースタスクが既に同期的に完了している場合、AsResultは非同期機構を使わずに同期的に返します。


Promise<T> — 手動完了ソース

ValkarnTask.Promise<T>ValkarnTask<T>がいつ完了するかを制御する必要があり、ライフタイムが単一の非同期操作に限定されない場合のために、ヒープアロケーションされた手動完了ソースです。

public class Promise<T>
{
public ValkarnTask<T> Task { get; }
public bool TrySetResult(T result);
public bool TrySetException(Exception ex);
public bool TrySetCanceled(CancellationToken ct = default);
}
// コールバックベースのAPIのラップ
var promise = new ValkarnTask.Promise<string>();

SomeCallbackApi.OnComplete += value => promise.TrySetResult(value);
SomeCallbackApi.OnError += ex => promise.TrySetException(ex);

string result = await promise.Task;

PooledPromise<T>とは異なり、Promise<T>はプーリングされません。タスクがフォルトして呼び出し元がawaitしなかった場合の未処理例外の検出と報告にファイナライザーを使用します。

高頻度パターン(プロデューサー/コンシューマーループ、フレームごとの操作)には、GetResultが呼ばれた後に自動的にプールに返却されるValkarnTask.PooledPromise<T>を優先してください。


PooledPromise<T> — プールされた手動完了ソース

public sealed class PooledPromise<T> : ValkarnTask.ISource<T>, IPoolNode<PooledPromise<T>>
{
public static PooledPromise<T> Create(out uint token);
public static PooledPromise<T> CreateCompleted(T result, out uint token);
public ValkarnTask<T> Task { get; }
public bool TrySetResult(T result);
public bool TrySetException(Exception ex);
public bool TrySetCanceled(CancellationToken ct = default);
}

バッキングタスクのGetResultが呼ばれた後、プロミスはValkarnTaskCompletionCore<T>をリセットして自身をプールに返却します。ダブルリターンガードにより、GetResultが並行して呼ばれても一度しか行われないことを保証します。

// パターン: 準備ができたときに完了するValkarnTask<T>を生成
var promise = ValkarnTask.PooledPromise<int>.Create(out uint token);
ValkarnTask<int> task = promise.Task;

// 非同期で作業をディスパッチ
ThreadPool.QueueUserWorkItem(_ =>
{
int result = DoWork();
promise.TrySetResult(result);
});

// コンシューマーがawait;完了時にプロミスはプールに自動返却
int value = await task;

ValkarnTask<T>を取得する方法のまとめ

メソッド使用する場面
async ValkarnTask<T>内でのreturn value通常の非同期メソッド
ValkarnTask.FromResult(value)同期的な高速リターン
ValkarnTask.FromException<T>(ex)事前フォルトタスク
ValkarnTask.FromCanceled<T>(ct)事前キャンセルタスク
ValkarnTask.Run<T>(Func<T>, ...)スレッドプールオフロード
ValkarnTask.Run<T>(Func<ValkarnTask<T>>, ...)非同期スレッドプール作業
ValkarnTask.WhenAll<T1,T2>(t1, t2)2つの型付きタスクを待機してタプルを取得
ValkarnTask.WhenAll<T>(IEnumerable<...>)N個の型付きタスクを待機して配列を取得
ValkarnTask.WhenAny<T>(t1, t2)2つの型付きタスクのうち最初のもの
ValkarnTask.WhenAny<T>(IEnumerable<...>)N個の型付きタスクのうち最初のもの
task.AsResult<T>()例外安全なラッピング
new ValkarnTask.Promise<T>().Task長期的な手動完了
ValkarnTask.PooledPromise<T>.Create(...).Task高頻度の手動完了