.NET の初期バージョンでは、スレッドのみに依存できました (スレッドは直接作成することも、ThreadPool クラスを使用して作成することもできました)。ThreadPool クラスはマネージ抽象化レイヤーを提供しますが、開発者はさらに詳細な制御のために Thread クラスに依存する必要があります。Thread クラスは維持が難しく、管理できないため、メモリとCPUに大きな負担がかかります。
したがって、Thread クラスの利点を最大限に活用し、その問題を回避できるソリューションが必要です。これがタスク(タスク)です。
(また: この章は比較的大きいため、3 部構成で公開されます。)
1. タスクの特徴
タスク (Task) は、.NET の抽象化であり、非同期単位です。技術的に言えば、タスクはスレッドの単なるラッパーであり、このスレッドは ThreadPool を通じて作成されます。ただし、タスクには待機、キャンセル、続行などの機能があり、タスクの完了後に実行できます。
タスクには次の重要なプロパティがあります。
-
タスクは TaskScheduler (タスク スケジューラ) によって実行され、デフォルトのスケジューラは ThreadPool 上でのみ実行されます。
-
タスクから値を返すことができます。
-
タスクは完了すると通知されます (ThreadPool または Thread ではありません)。
-
タスクの継続的な実行は、ContinueWith() を使用して構築できます。
-
Task.Wait() を呼び出すことでタスクの実行を待つことができます。これにより、タスクが完了するまで呼び出し元のスレッドがブロックされます。
-
タスクを使用すると、従来のスレッドや ThreadPool よりもコードを読みやすくできます。また、C# 5.0 での非同期プログラミング構造の導入への道も切り開きました。
-
タスクが別のタスクから開始されると、それらの間に親子関係を確立できます。
-
サブタスクからの例外は親タスクに伝播できます。
-
タスクは、 cancelToken クラスを使用してキャンセルできます。
2. タスクを作成して開始する
タスク並列ライブラリ (TPL) を使用して、いくつかの方法でタスクを作成および実行できます。
2.1. タスクの使用
Task クラスは、ThreadPool スレッドとして非同期に作業を実行する方法です。タスクベースの非同期パターン (Task-Based Asynchronous Pattern、TAP) を使用します。非ジェネリックの Task クラスは結果を返さないため、タスクから値を返す必要がある場合は、ジェネリック バージョンの Task<T> を使用する必要があります。タスクは、Start メソッドを呼び出して実行をスケジュールする必要があります。
具体的なタスク呼び出しコードは次のとおりです。
/// <summary>
/// 测试方法,打印10次,等待10秒
/// </summary>
public static void DebugAndWait()
{
int length = 10;
for (int i = 0; i < length; i++)
{
Debug.Log($"执行第:{i + 1}/{length} 次打印!");
Thread.Sleep(1000);
}
}
//使用任务执行
private void RunByNewTask()
{
//创建任务
Task task = new Task(TestFunction.DebugAndWait);
task.Start();//不调用 Start 则不会执行
}
最終結果は驚くことではありません。
2.2、使用 Task.Factory.StartNew
TaskFactory クラスの StartNew メソッドでもタスクを作成できます。この方法で作成されたタスクは ThreadPool で実行されるようにスケジュールされ、タスクへの参照が返されます。
private void RunByTaskFactory()
{
//使用 Task.Factory 创建任务,不需要调用 Start
var task = Task.Factory.StartNew(TestFunction.DebugAndWait);
}
もちろん、印刷結果は上記と同じです。
2.3、Task.Run の使用
原則は Task.Factory.StartNew と同じです。
private void RunByTaskRun()
{
//使用 Task.Run 创建任务,不需要调用 Start
var task = Task.Run(TestFunction.DebugAndWait);
}
2.4、タスクの遅延
Task.Delay を使用してタスクを作成することもできますが、このタスクは少し特殊です。指定した時間間隔後に実行できます。次を使用できます。
CacellationToken クラスはいつでもキャンセルされます。Thread.Sleep とは異なり、Task.Delay は CPU サイクルを使用せず、非同期で実行できます。
2 つの違いを反映するために、例を直接書きます。
public static void DebugWithTaskDelay()
{
Debug.Log("TaskDelay Start");
Task.Delay(2000);//等待2s
Debug.Log("TaskDelay End");
}
次に、このメソッドをプログラム内で直接同期的に呼び出します。
private void RunWithTaskDelay()
{
Debug.Log("开始测试 Task.Delay !");
TestFunction.DebugWithTaskDelay();
Debug.Log("结束测试 Task.Delay !");
}
結果は次のとおりです。
まったく待たされることなく、4枚のプリントが順番に瞬時に出力されたことがわかります。上記の Task.Delay を Thread.Sleep に置き換えるとどうなるでしょうか?
このメソッドを実行した後、Unity が直接スタックし、2 秒後に 4 つのメッセージが出力されました。そして、明らかにスレッド待機が有効になりますが、メインスレッドをブロックすることによって有効になります。
Task.Delay に戻り、Task.Run を使用してこのメソッドを実行し、次のように結果を出力しましょう。
明らかに、スレッドはコマンドが有効になるまで待機します。これは、子スレッドの遅延が正常に動作できることを示しています。
2.5、タスクの収量
Task.Yiled は待機タスクを作成するもう 1 つの方法です。このメソッドを使用して、メソッドを強制的に非同期にし、制御をオペレーティング システムに戻します。
どうやって理解しますか?ここでは非常に時間のかかる関数が必要です。
public static async void DebugWithTaskYield()
{
int length = 27;//这个方法不能执行很多次
string str = "";
for (int i = 0; i < length; i++)
{
//以下是耗时函数
str += "1,1";
var arr = str.Split(',');
foreach (var item in arr)
{
str += item;
}
await Task.Yield();
Debug.Log($"执行第:{i + 1}/{length} 次打印!");
}
}
ここでは、単純な文字列のスプライシングを直接使用して、時間のかかる関数を実装しています。
メインスレッドで Task.Run を呼び出して実行すると、デバッグ結果は次のようになります。
文字列の増加に伴い、1回にかかる時間がどんどん長くなっていることがわかります。ただし、1 回の時間がどれだけ長くても、メインスレッドは妨げられません。最初は Unity のコルーチンと同じように感じるかもしれませんが、Unity のコルーチンはメインスレッド上で動作するものであり、コルーチンを使用してもメインスレッドがブロックされないわけではありません。ここでは、コルーチンのロジックを使用してこのコードを直接実装します。
public static IEnumerator DebugWithCoroutine()
{
int length = 27;//这个方法不能执行很多次
string str = "";
for (int i = 0; i < length; i++)
{
//以下是耗时函数
str += "1,1";
var arr = str.Split(',');
foreach (var item in arr)
{
str += item;
}
yield return null;
Debug.Log($"执行第:{i + 1}/{length} 次打印!");
}
}
ロジックに違いはなく、await Task.Yield(); を yield return null に変更するだけです。もちろん、ログの出力は似ていますが、メインスレッドには本質的な違いがあります。最後まで実行すると、反復ごとにメインスレッドがフリーズします。これはプロファイラーで非常に明白に見えます。
(コルーチン呼び出しには明らかに時間がかかることがわかります)
2.6、Task.FromResult
FromResult<T> は、.NET Framework 4.5 で導入されたメソッドであり、Unity 2022.2.5 f1c1 で使用される .NET Standard 2.1 でサポートされています。
public static int FromResultTest()
{
int length = 100;
int result = 0;
for (int i = 0; i < length; i++)
result += Random.Range(0, 100);
Debug.Log($"FromResultTest 运算结果:{result} ");
return result;
}
private void RunWithFromResult()
{
Debug.Log("RunWithFromResult Start !");
Task<int> resultTask = Task.FromResult<int>(TestFunction.FromResultTest());
Debug.Log("RunWithFromResult End ! Result : " + resultTask.Result);
}
上記のコードに示すように、RunWithFromResult の結果は次のようになります。
一般的な非同期タスクとは異なり、実行順序に従って順次出力されます。この関数が時間のかかる関数である場合、メインスレッドがブロックされますか? 2.5 でテストされた時間のかかる関数をテスト用に移動しました (コードは投稿されません)。
明らかにメインスレッドがブロックされています。
つまり、この FromResult は、スケジューリングのために非同期メソッドをメインスレッドに取り込みます (子スレッドを直接親スレッドに取り込むとも理解できます)。すでに Unity のメイン スレッドであるため、Task.Delay は有効になりませんが、Thread.Sleep が有効になり、メイン スレッドがブロックされます。
Task タスクを作成する以前の方法とは異なり、この Task.FromResult はパラメーター付きの関数を呼び出すことができます (Task.Run はパラメーターなしの関数のみを実行できます)。ただし、そうであっても、親スレッドをブロックしてしまうため、メインの Unity スレッドで使用することはお勧めできません。
2.7、Task.FromException と Task.FromException<T>
これらのメソッドは両方とも、非同期タスクで例外をスローできるため、単体テストに役立ちます。
(ここでは当面使用しないので説明しません。この 2 つについては、後で単体テストを学習するときに詳しく学習します)
2.8、Task.FromCanceled と Task.FromCanceled<T>
これは Task.FromException の状況と少し似ていますが、どれも役に立たないように見えて、実際には非常に便利なメソッドです。学習の便宜上、ここではまだ説明します。
まず、次のコード部分を見てください。これは、Task.FromCanceled のサンプル コードでもあります。
CancellationTokenSource source = new CancellationTokenSource();//构建取消令牌源
source.Cancel();//设置为取消
//返回标记为取消的任务。
//注意!使用此方法要确保 CancellationTokenSource 已经调用过 Cancel 方法 ,否则会出错!
Task.FromCanceled(source.Token);
最終的なタスクのステータス (Task.Status) を出力すると、結果は Created になります。
これは何の役に立つの?と誰かが必ず尋ねるでしょう。キャンセルされたタスクを作成しましたか? では、このコードを実行する意味は何でしょうか?
このコードだけを見ると本当に意味がありませんが、ここで要件を提案します。
ロジックは非常に単純ですが、問題は最後のタスクの維持にあります。実行が予想されるタスク A は長期の非同期関数であり、外部からそのステータスと結果を検出する必要があると仮定します。では、偶数を入力した場合は何を返せばよいのでしょうか? まず、空のTaskを返してはいけません。この戻りは通常のTaskと同じで、外部監視ステータスはWaitingToRun、RanToCompletion、Runningのいずれかです。タスク A を実行したかどうかを知る方法はありません。
この時点で、Task.FromCanceled の役割を見つけました。
private void RunWithFromCanceled()
{
var val = commonPanel.GetInt32Parameter();
//这里测试输入双数就取消执行,单数就正常执行。
CancellationTokenSource source = new CancellationTokenSource();
if (val % 2 == 0)
source.Cancel();
var task = TestFunction.TestCanceledTask(source);
Debug.Log($"Task State 1: {task.Status}");
}
/// <summary>
/// 测试用于取消任务
/// </summary>
public static Task TestCanceledTask(CancellationTokenSource source)
{
if (source.IsCancellationRequested)
{
Debug.Log($"任务取消 !");
var token = source.Token;
return Task.FromCanceled(token);
}
else
{
Debug.Log($"任务执行 !");
return Task.Run(DebugWithTaskDelay);
}
}
偶数を入力するとキャンセルされたタスクが返され、奇数を入力すると通常どおり実行されます。
タスクをカプセル化すると、内部の判断ロジックはより複雑になり、外部は内部ロジックを知らなくてもタスクの実行だけを知る必要があります。このとき、共通の「異常」なTaskを外部に返すにはTask.FromCanceledとTask.FromExceptionを利用します。
3. 完了したタスクから結果を取得する
タスク並列ライブラリ (TPL) で提供される API は次のとおりです。
/// <summary>
/// 获取任务并行结果
/// </summary>
private void GetTaskResult()
{
int inputParam = commonPanel.GetInt32Parameter();
Debug.Log($"get task result start ! paramter : {inputParam}");
//方法1 :new Task
var task_1 = new Task<int>(()=>TestFunction.FromResultTest(inputParam));
task_1.Start();
Debug.Log($"task_1 result : {task_1.Result}");
//方法2:Task.Factory
var task_2 = Task.Factory.StartNew<int>(()=> TestFunction.FromResultTest(inputParam));
Debug.Log($"task_2 result : {task_2.Result}");
//方法3:
var task_3 = Task.Run<int>(()=>TestFunction.FromResultTest(inputParam));
Debug.Log($"task_3 result : {task_3.Result}");
//方法4:
var task_4 = Task.FromResult<int>(TestFunction.FromResultTest(inputParam));
Debug.Log($"task_4 result : {task_4.Result}");
}
このテストでは、最終的におなじみのエラーが表示されました。
Random.Range は Unity メインスレッドからのみ使用できます。
UnityEngine クラスはサブスレッドでは使用できないことが知られており、ここでそれが発生します。しかし、それは問題ではありません。System の Random を使用するだけで、このメソッドを直接変更できます。
しかし、これはプログラムが確かに子スレッドで実行されていることを示していますが、実際にはこれら 4 つのメソッドがメイン スレッドをブロックします。
すべての計算プロセスは 2.6 の FromResult と同じであり、子スレッドはメインスレッドに転送されて使用されます。明らかに、これらのメソッドは同期的な結果取得を提供しますが、実際の非同期計算をこの方法で直接使用することはできません。
スペースの制限により、タスクの並列処理 (オン) はここで終了します。