307 Views
March 28, 25
スライド概要
2025/03/06に開催されたイベント、「ぷちTechCon for Unity」 で発表したスライドです。
イベント概要:https://dena.connpass.com/event/339748/
C# の非同期プログラミングを支える async/await の裏側の仕組みについてご存知でしょうか? 今回のLTでは、async/awaitの背後でどのような処理が行われているのかのエッセンスを紹介します。また、Unity 環境で高パフォーマンスな非同期処理を実現する UniTask がどのようにして効率化しているのかを少し紹介します。
DeNA が社会の技術向上に貢献するため、業務で得た知見を積極的に外部に発信する、DeNA 公式のアカウントです。DeNA エンジニアの登壇資料をお届けします。
C#のasync/await Deep Dive 〜UniTaskを添えて〜 bo40 株式会社ディー・エヌ・エー © DeNA Co., Ltd. 1
自己紹介 bo40 分野にこだわりなくプログラムを書いて遊んでいる Unityはよく知らない 普段はOCamlと戯れている @bo40 © DeNA Co., Ltd. 2
C#のasync/await Deep Dive
C# の非同期処理はシンプルに async/await で記述できますが、その裏側の仕組みを知って
いますか?
今日はその仕組みの深堀りしつつ、UniTask が何をどのようにして効率化しているのか、そ
のエッセンスを紹介します!
public async Task<string> FetchDataAsync()
{
var data = await GetDataFromServerAsync();
return ProcessData(data);
}
© DeNA Co., Ltd.
3
async/await のキホン
async メソッドは Task や UniTask を返す
public async UniTask<string> FetchDataAsync()
{
var data = await GetDataFromServerAsync();
return ProcessData(data);
}
await キーワードで非同期処理の完了を待機できる
© DeNA Co., Ltd.
4
async/await のギモン
TaskやUniTaskって何者?
asyncの返り値のルールってなに?
async メソッドは Task や UniTask を返す
public async UniTask<string> FetchDataAsync()
{
var data = await GetDataFromServerAsync();
return ProcessData(data);
}
一体どうやって実行を中断し、
中断した位置から再開できるのか?
await キーワードで非同期処理の完了を待機できる
© DeNA Co., Ltd.
5
asyncメソッドの戻り値の型に関する制約
●
asyncメソッドの戻り値の型
○
●
void, Taskなどに加えて、タスクライクな型、IAsyncEnumerableなど
タスクライクな型
○
GetAwaiter()メソッドを持ち、Awaiterを返すこと
○
AsyncMethodBuilderを提供していること
→UniTaskは上述のタスクライクな型
[AsyncMethodBuilder(typeof(MyTaskMethodBuilder.>))]
class MyTask<T>
{
public Awaiter<T> GetAwaiter() { ... }
}
class Awaiter<T> : ICriticalNotifyCompletion
{
public void UnsafeOnCompleted(Action completion) { ... }
}
参考 : https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/classes#1515-async-functions
© DeNA Co., Ltd.
6
awaitで中断し、止まったところから再開するというのを
どうやっているのか?
●
C#コンパイラが、async/awaitを使った構文をステートマシンに変換
●
ステートマシン
○
どのawaitまで実行したかを状態として持ち、中断した位置から再実行できる
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
return result;
}
初期状態
1つ目のawaitまで
1つ目のawaitま
での間の処理
の間の処理
State0
1つ目のawaitまで
実行した状態
State1
2つ目のawaitまで
実行した状態
2つ目のawaitまで
の間の処理
3つ目のawaitより
後の処理
終了状態
© DeNA Co., Ltd.
7
変換後のC#コードを見てみよう © DeNA Co., Ltd. 8
具体的にどんなコードに変換されている?
public async UniTask<string> sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
© DeNA Co., Ltd.
return result;
9
具体的にどんなコードに変換されている?
public async UniTask<string> sampleUniTaskAsyncMethod()
{
注目ポイント
var result = "Hello";
await UniTask.Delay(1000);
ローカル変数 result がawaitを
result += " World!";
await UniTask.Delay(1000);
}
© DeNA Co., Ltd.
またがってアクセスされている
return result;
10
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
メソッドの本体の変換
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
return result;
asyncキーワードが消えている
public UniTask<string> sampleUniTaskAsyncMethod()
{
var stateMachine = new AsyncAwaitSampleSM();
stateMachine.builder = AsyncUniTaskMethodBuilder<string>.Create();
stateMachine.thisReference = this;
stateMachine.state = -1;
状態の初期値
stateMachine.builder.Start(ref stateMachine);
は -1
return stateMachine.builder.Task;
}
ステートマシンのクラス
AsyncAwaitSampleSM を生成している
Start()でstateMachineのMoveNext()
が呼ばれる
© DeNA Co., Ltd.
11
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
生成されたステートマシンAsyncAwaitSampleSMの実装
(IAsyncStateMachineインターフェースを実装している)
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
private class AsyncAwaitSampleSM : IAsyncStateMachine
{
状態ID
public int state;
public AsyncUniTaskMethodBuilder<string> builder;
public AsyncAwaitSample thisReference;
private string localVarResult;
private UniTask.Awaiter awaiter;
return result;
awaitをまたぐようなロー
カル変数をメンバー変数と
してキャプチャしている
© DeNA Co., Ltd.
}
void MoveNext()
{
./ ステートマシンの実際の動きのコード
}
12
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
ステートマシンAsyncAwaitSampleSM の
MoveNext()にそれぞれのステップがある
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
return result;
© DeNA Co., Ltd.
private class AsyncAwaitSampleSM : IAsyncStateMachine
{
...
void MoveNext()
{
int state = this.state;
try
{
...
if (state .= 0)
{
if (state .= 1)
{
./ state .= -1 (非同期処理の開始)
this.localVarResult = "Hello";
awaiter1 = UniTask.Delay(1000, ...).GetAwaiter();
if (!awaiter1.IsCompleted)
{
./ 1つめのawaitの処理が完了していないとき
./ stateを1つ目awaitの位置を表す 0 にしてreturnする
this.state = 0;
...
this.builder.AwaitUnsafeOnCompleted<...>(ref awaiter1, ref stateMachine);
return;
}
}
else
{
./ state .= 1 (2つめのawaitの後の非同期処理の再開)
awaiter2 = this.awaiter;
this.awaiter = new UniTask.Awaiter();
this.state = -1;
goto label_9;
}
}
else
{
./ state .= 0 (1つめのawaitの後の非同期処理の再開)
...
this.state =-1;
}
...
this.localVarResult = string.Concat(this.localVarResult, " World!");
awaiter2 = UniTask.Delay(1000, false, PlayerLoopTiming.Update, new CancellationToken(), false).GetAwaiter();
if (!awaiter2.IsCompleted)
{
./ 2つめのawaitの処理が完了していないとき
./ stateを1つ目awaitの位置を表す 1 にしてreturnする
this.state = 1;
...
this.builder.AwaitUnsafeOnCompleted<UniTask.Awaiter, AsyncAwaitSampleSM>(ref awaiter2, ref stateMachine);
return;
}
label_9:
...
result51 = this.localVarResult;
}
catch (Exception ex)
{
...
}
this.state = -2;
this.builder.SetResult(result51);
}
}
13
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
MoveNext()の中身
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
return result;
stateに1つ目awaitの位置を
表す 0 を入れてreturnする
© DeNA Co., Ltd.
void IAsyncStateMachine.MoveNext()
{
int state = this.state;
...
if (state .= 0)
{
if (state .= 1)
{
./ state .= -1 (非同期処理の開始)
this.localVarResult = "Hello";
awaiter1 = UniTask.Delay(1000, ...).GetAwaiter();
if (!awaiter1.IsCompleted)
{
this.state = 0;
...
this.builder.AwaitUnsafeOnCompleted<...>(ref awaiter1,
ref stateMachine);
return;
}
}
...
stateの初期値
は -1
1つめのawaitの処理が
完了していないとき
1つ目のawaitが完了したら、再度
MoveNext()を呼ぶように指定
14
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
MoveNext()の中身
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
return result;
stateに2つ目awaitの位置を
表す 1 を入れてreturnする
© DeNA Co., Ltd.
void IAsyncStateMachine.MoveNext()
{
int state = this.state;
...
if (state .= 0) { ... }
else
{
./ state .= 0 (1つめのawaitの後の非同期処理の再開)
...
this.state =-1;
}
...
this.localVarResult =
string.Concat(this.localVarResult, " World!");
awaiter2 = UniTask.Delay(1000, ...).GetAwaiter();
if (!awaiter2.IsCompleted)
{
this.state = 1;
...
this.builder.AwaitUnsafeOnCompleted<...>(ref awaiter2,
ref stateMachine);
return;
}
...
1つ目のawaitで
stateが0に更新済
2つめのawaitの処理が
完了していないとき
2つ目のawaitが完了したら、再度
MoveNext()を呼ぶように指定
15
変換後のLow Level C# (RiderのIL Viewerで見ることができる)
※注意 : コード量が多いのでかなり簡略化しています
MoveNext()の中身
public async UniTask<string>
sampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
}
return result;
stateの値を終了状態
を表す -2 に更新
© DeNA Co., Ltd.
void IAsyncStateMachine.MoveNext()
{
int state = this.state;
...
if (state .= 0)
{
if (state .= 1) { ... }
else
{
./ state .= 1 (2つめのawaitの後の非同期処理の再開)
...
this.state = -1;
goto label_9;
}
}
else { ... }
...
label_9:
...
result51 = this.localVarResult;
...
this.state = -2;
this.builder.SetResult(result51);
}
1つ目のawaitで
stateが1に更新済
このメソッドの
一番末尾にある
ラベルへジャンプ
ローカル変数result
を読み出して返り値
をセット
16
UniTaskは何がどう効率的なのか? © DeNA Co., Ltd. 17
UniTask は何を効率化したのか? ● メモリアロケーションの効率化 ○ UnityはGC(ガベージコレクション)の負荷が重い 特にヒープへのオブジェクトの割り当てが多いとGC頻度が増す ○ ● Taskはクラスであり、非同期操作ごとにヒープに割り当てされる 不要なコンテキストの省略 ○ SynchronizationContext や ExecutionContext ■ © DeNA Co., Ltd. (時間の都合上、今回は触れず) 18
UniTaskはどうやって効率化したのか? ● UniTask, Awaiter(UniTaskのGetAwaiter()メソッドの返り値)などを クラス(参照型)ではなくstruct(値型)で定義 ○ ● ヒープではなくスタックに割り当てられる カスタムのAsyncTaskMethodBuilder(AsyncUniTaskMethodBuilder)を実装し、不要な アロケーションや処理を削減し、オーバーヘッドを低減 他にも、Unityで便利なメソッドがたくさん実装されているのも嬉しいですね © DeNA Co., Ltd. 19
C#のasync/awaitについて、 ちょっと理解が深まりましたか? © DeNA Co., Ltd. 20
もっと詳しく知りたくなったら? ● 公式ブログ ○ How Async/Await Really Works in C# https://devblogs.microsoft.com/dotnet/how-async-await-really-works/ ● UniTask v2 ○ © DeNA Co., Ltd. https://tech.cygames.co.jp/archives/3417/ 21
Appendix © DeNA Co., Ltd. 22
awaitが3つ以上の場合のLow Level C#
MoveNext()の中身が switch文 (とgoto)になり
●
少し読みやすくなります
private static async UniTask<string>
SampleUniTaskAsyncMethod()
{
var result = "Hello";
await UniTask.Delay(1000);
result += " World!";
await UniTask.Delay(1000);
result += " UniTask!";
await UniTask.Delay(1000);
}
© DeNA Co., Ltd.
return result;
void IAsyncStateMachine.MoveNext()
{
int num1 = this.state;
string result51;
try
{
UniTask uniTask;
UniTask.Awaiter awaiter1;
UniTask.Awaiter awaiter3;
switch (num1)
{
case 0:
awaiter1 = this.awaiter;
this.awaiter = new UniTask.Awaiter();
break;
case 1:
awaiter2 = this.awaiter;
this.awaiter = new UniTask.Awaiter();
goto label_8;
case 2:
awaiter3 = this.awaiter;
this.awaiter = new UniTask.Awaiter();
goto label_11;
default:
this.localVarResult = "Hello";
uniTask = UniTask.Delay(1000, false, PlayerLoopTiming.Update, new CancellationToken(), false);
awaiter1 = uniTask.GetAwaiter();
if (!awaiter1.IsCompleted)
{
this.awaiter = awaiter1;
AsyncAwaitSampleSM stateMachine = this;
this.builder.AwaitUnsafeOnCompleted<UniTask.Awaiter, AsyncAwaitSampleSM>(ref awaiter1, ref stateMachine);
return;
}
break;
}
awaiter1.GetResult();
this.localVarResult = string.Concat(this.localVarResult, " World!");
uniTask = UniTask.Delay(1000, false, PlayerLoopTiming.Update, new CancellationToken(), false);
awaiter2 = uniTask.GetAwaiter();
if (!awaiter2.IsCompleted)
{
this.awaiter = awaiter2;
AsyncAwaitSampleSM stateMachine = this;
this.builder.AwaitUnsafeOnCompleted<UniTask.Awaiter, AsyncAwaitSampleSM>(ref awaiter2, ref stateMachine);
return;
}
label_8:
awaiter2.GetResult();
this.localVarResult = string.Concat(this.localVarResult, " UniTask!");
uniTask = UniTask.Delay(1000, false, PlayerLoopTiming.Update, new CancellationToken(), false);
awaiter3 = uniTask.GetAwaiter();
if (!awaiter3.IsCompleted)
{
this.state = num2 = 2;
this.awaiter = awaiter3;
AsyncAwaitSampleSM stateMachine = this;
this.builder.AwaitUnsafeOnCompleted<UniTask.Awaiter, AsyncAwaitSampleSM>(ref awaiter3, ref stateMachine);
return;
}
label_11:
awaiter3.GetResult();
result51 = this.localVarResult;
}
catch (Exception ex)
{
this.state = -2;
this.localVarResult = (string) null;
this.builder.SetException(ex);
return;
}
this.state = -2;
this.localVarResult = (string) null;
this.builder.SetResult(result51);
}
switch文
23
© DeNA Co., Ltd. 24