【Unite Tokyo 2018】さては非同期だなオメー!async/await完全に理解しよう

5.3K Views

May 09, 18

スライド概要

講演者:名雪 通(ユニティ・テクノロジーズ・ジャパン合同会社)
   :安原 祐二(ユニティ・テクノロジーズ・ジャパン合同会社)

こんな人におすすめ
・C#プログラマー
・.NETで開発経験があり、これからUnityを使い始めるプログラマー
・「async/await完全に理解した」と言いたい人

受講者が得られる知見
・async/awaitの知識

profile-image

リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

2018/05/07 - 09 さては非同期だなオメー! async/await完全に理解しよう 名雪 通 ユニティ・テクノロジーズ・ジャパン

2.

名雪 通 エンジニア asyncのほう。

3.

安原 祐二 エンジニア awaitのほう。

4.

#unitetokyo2018 #完全に理解した

5.

2018年5月

6.

Unity 2018.1 リリース

7.

.NET 4.xがExperimental→Stableに!

8.

C# 4.0→6.0

9.

参考: C# 5.0の新機能 • async/await • 呼び出し元情報属性

10.

参考: C# 6.0の新機能 • 読み取り専用の自動プロパティ • 自動プロパティの初期化子 • 式形式の関数メンバー • using static • null条件演算子 • 文字列補間 • 例外フィルター • nameof式 • catchブロックとfinallyブロックでのawait • インデックス初期化子 • コレクション初期化子の拡張メソッド • オーバーロード解決の改善

11.

async/await

12.

async/awaitとは何か

13.

async(= asynchronous = 非同期) awaitを使うメソッドにつける必要があるキーワード

14.

await(= 待つ) 非同期(async)メソッドを呼び出し、 その完了まで実行を中断するキーワード

15.

ここまででのasync/await理解率: 10%

16.

ここで問題です

17.

このコードの開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); Thread.Sleep(5000); // 終了 Console.WriteLine(DateTime.Now); }

19.

普通に5秒

20.

次の問題

21.

このコードは開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); AsyncMethod(); // 終了 Console.WriteLine(DateTime.Now); } static async void AsyncMethod() { Thread.Sleep(5000); }

22.

やっぱり5秒

23.

別にasyncをつけただけで非同期になるわけではない

24.

次はawaitの問題

25.

このコードは開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }

27.

なんと0秒

28.

次の問題

29.

このコードは開始→終了まで何秒かかるでしょうか static async void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); await Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }

30.

5秒になった!

31.

async/awaitは 「処理を非同期に行うための仕組み」ではなく、 「非同期の処理を待つための仕組み」である 理解率: 50%

32.

では、どのように その「非同期の処理を待つための仕組み」を 実現しているのか?

33.

逆コンパイルしてみよう

34.

さっきのコード static async void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); await Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }

35.
[beta]
逆コンパイルした結果
private static void AsyncTest() {
<AsyncTest>d__1 <AsyncTest>d__ = new <AsyncTest>d__1();
<AsyncTest>d__.<>t__builder = AsyncVoidMethodBuilder.Create();
<AsyncTest>d__.<>1__state = -1;
AsyncVoidMethodBuilder <>t__builder = <AsyncTest>d__.<>t__builder;
<>t__builder.Start(ref <AsyncTest>d__);
}

36.
[beta]
インナークラスも生成されている
private sealed class <AsyncTest>d__1 : IAsyncStateMachine {
public int <>1__state;
public AsyncVoidMethodBuilder <>t__builder;
private TaskAwaiter <>u__1;
private void MoveNext() {
int num = <>1__state;
try {
TaskAwaiter awaiter;
if (num != 0) {
Console.WriteLine(DateTime.Now);
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.get_IsCompleted()) {
num = (<>1__state = 0);
<>u__1 = awaiter;
<AsyncTest>d__1 <AsyncTest>d__ = this;
<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter,
<AsyncTest>d__1>(ref awaiter, ref <AsyncTest>d__);
return;
}

} else {
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
Console.WriteLine(DateTime.Now);
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
}

37.

わかりやすく書き直してみる

38.

元の関数 private static void AsyncTest() { var stateMachine = new AsyncTestStateMachine(); stateMachine.builder = AsyncVoidMethodBuilder.Create(); stateMachine.state = -1; stateMachine.builder.Start(ref stateMachine); } static async void AsyncTest() { Console.WriteLine(DateTime.Now); // 開始 await Task.Delay(5000); Console.WriteLine(DateTime.Now); // 終了 }

39.
[beta]
インナークラス
private sealed struct AsyncTestStateMachine : IAsyncStateMachine {
public int state;
public AsyncVoidMethodBuilder builder;
private TaskAwaiter taskAwaiter;
private void MoveNext() {
int num = state;
try {
TaskAwaiter awaiter;
if (num != 0) {
Console.WriteLine(DateTime.Now);
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted) {
num = state = 0;
taskAwaiter = awaiter;
AsyncTestStateMachine stateMachine = this;
builder.AwaitUnsafeOnCompleted<TaskAwaiter, AsyncTestStateMachine>(ref
awaiter, ref stateMachine);
return;
}
} else {
awaiter = taskAwaiter;
taskAwaiter = default(TaskAwaiter);
num = state = -1;
}
awaiter.GetResult();
Console.WriteLine(DateTime.Now);
}

catch (Exception exception)
{
state = -2;
builder.SetException(exception);
return;
}
state = -2;
builder.SetResult();
}
}

40.
[beta]
インナークラスのMoveNext
private sealed struct AsyncTestStateMachine : IAsyncStateMachine {
public int state;
public AsyncVoidMethodBuilder builder;
private TaskAwaiter taskAwaiter;
private void MoveNext() {
int num = state;
try {
TaskAwaiter awaiter;
if (num != 0) {
Console.WriteLine(DateTime.Now);
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted) {
num = state = 0;
taskAwaiter = awaiter;
AsyncTestStateMachine stateMachine = this;
builder.AwaitUnsafeOnCompleted<TaskAwaiter, AsyncTestStateMachine>(ref
awaiter, ref stateMachine);
return;
}
} else {
awaiter = taskAwaiter;
taskAwaiter = default(TaskAwaiter);
num = state = -1;
}
awaiter.GetResult();
Console.WriteLine(DateTime.Now);
}

catch (Exception exception)
{
state = -2;
builder.SetException(exception);
return;
}
state = -2;
builder.SetResult();

private void MoveNext() {
}

}

41.
[beta]
インナークラス
private sealed struct AsyncTestStateMachine : IAsyncStateMachine {
public int state;
public AsyncVoidMethodBuilder builder;
private TaskAwaiter taskAwaiter;

catch (Exception exception)
{
state = -2;
builder.SetException(exception);
return;
}
state = -2;
builder.SetResult();

TaskAwaiter awaiter;
if (num != 0) {
}
Console.WriteLine(DateTime.Now);
}
awaiter = Task.Delay(5000).GetAwaiter();

private void MoveNext() {
int num = state;
try {
TaskAwaiter awaiter;
if (num != 0) {
Console.WriteLine(DateTime.Now);
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted) {
num = state = 0;
taskAwaiter = awaiter;
AsyncTestStateMachine stateMachine = this;
builder.AwaitUnsafeOnCompleted<TaskAwaiter, AsyncTestStateMachine>(ref
awaiter, ref stateMachine);
return;
}
} else {
awaiter = taskAwaiter;
taskAwaiter = default(TaskAwaiter);
num = state = -1;
}
awaiter.GetResult();
Console.WriteLine(DateTime.Now);
}

Console.WriteLine(DateTime.Now);

42.
[beta]
関数を抜けてる!
private sealed struct AsyncTestStateMachine : IAsyncStateMachine {
public int state;
public AsyncVoidMethodBuilder builder;
private TaskAwaiter taskAwaiter;
private void MoveNext() {
int num = state;
try {
TaskAwaiter awaiter;
if (num != 0) {
Console.WriteLine(DateTime.Now);
awaiter = Task.Delay(5000).GetAwaiter();
if (!awaiter.IsCompleted) {
num = state = 0;
taskAwaiter = awaiter;
AsyncTestStateMachine stateMachine = this;
builder.AwaitUnsafeOnCompleted<TaskAwaiter, AsyncTestStateMachine>(ref
awaiter, ref stateMachine);
return;
}
} else {
awaiter = taskAwaiter;
taskAwaiter = default(TaskAwaiter);
num = state = -1;
}
awaiter.GetResult();
Console.WriteLine(DateTime.Now);
}

return;

catch (Exception exception)
{
state = -2;
builder.SetException(exception);
return;
}
state = -2;
builder.SetResult();
}
}

43.

問題です

44.

Task.Delayの前後の実行スレッドは? static void Main(string[] args) { AsyncTest(); Console.ReadLine(); } static async void AsyncTest() { // 前 Console.WriteLine(Thread.CurrentThread.ManagedThreadId); await Task.Delay(5000); // 後 Console.WriteLine(Thread.CurrentThread.ManagedThreadId); }

45.

違う

46.

同じコードをUnityで実行すると? async void Start () { // 前 Debug.Log(Thread.CurrentThread.ManagedThreadId); await Task.Delay(5000); // 後 Debug.Log(Thread.CurrentThread.ManagedThreadId); }

47.

同じ!

48.

SynchronizationContextが await後のコードを実行するスレッドを決める CLIアプリケーションには SynchronizationContextが存在しないため 自動的にスレッドプール上で実行される

49.

UnityにはUnityの SynchronizationContextの実装がある 参考: https://github.com/Unity-Technologies/UnityCsReference/blob/ 83cceb769a97e24025616acc7503e9c21891f0f1/Runtime/Export/ UnitySynchronizationContext.cs

50.

もともとUnityには 非同期処理のための仕組みがあります

51.

コルーチン • C# 2.0の反復子(yield)を利用した継続処理の仕組み • 様々なタイミングを待つことができる(Unity側の仕組みによる) • フレームの終わり • FixedUpdate • 指定した秒数後(の最初のフレーム) • その他非同期処理(AsyncOperation) • 戻り値を返しにくい

52.

コルーチンの例 IEnumerator Coroutine() { // 前 Debug.Log(Thread.CurrentThread.ManagedThreadId); yield return new WaitForSeconds(0.5f); // 後 Debug.Log(Thread.CurrentThread.ManagedThreadId); }

53.
[beta]
逆コンパイルしてみよう
private IEnumerator Coroutine() {
return new <Coroutine>c__Iterator1();
}

//IL_0041: Expected O, but got Unknown
uint num = (uint)$PC;
$PC = -1;
switch (num) {
case 0u:
Debug.Log((object)Thread.CurrentThread.ManagedThreadId);
$current = (object)new WaitForSeconds(0.5f);
if (!$disposing) {
$PC = 1;
}
return true;
case 1u:
Debug.Log((object)Thread.CurrentThread.ManagedThreadId);
$PC = -1;
break;
}
return false;

private sealed class <Coroutine>c__Iterator1 : IEnumerator, IDisposable, IEnumerator<object> {
internal object $current;
internal bool $disposing;
internal int $PC;
object IEnumerator<object>.Current {
[DebuggerHidden]
get
{
return $current;
}
}
object IEnumerator.Current {
[DebuggerHidden]
get
{
return $current;
}
}

}
public void Dispose() {
$disposing = true;
$PC = -1;
}
public void Reset() {
throw new NotSupportedException();
}

public <Coroutine>c__Iterator1() {
}
}
public bool MoveNext() {
//IL_003c: Unknown result type (might be due to invalid IL or missing references)

54.

C# Job System • 2018.1の新機能 • クラスが使えない • Burstコンパイラーによる最適化の恩恵を受けられる • 処理はUnityのワーカースレッドで実行される

55.

awaitできるのはTaskだけではない

56.

TaskをawaitするのがTaskAwaiter Awaiterを作ればTaskでなくてもawaitできる

57.

awaitしたいクラスにGetAwaiter拡張メソッドを実装する public static class AsyncOperationAwaitable { public static Awaiter GetAwaiter(this AsyncOperation asyncOperation) { return new Awaiter(asyncOperation); } public void GetResult() { } public IEnumerator WrappedCoroutine() { yield return asyncOperation; continuation(); } public class AsyncOperationAwaiter : INotifyCompletion { private AsyncOperation asyncOperation; private System.Action continuation; } } public AsyncOperationAwaiter(AsyncOperation asyncOperation) { this.asyncOperation = asyncOperation; CoroutineDispatcher.Get().DispatchCoroutine(WrappedCoroutine()); } public bool IsCompleted { get { return asyncOperation.isDone; } } public void OnCompleted(System.Action continuation) { this.continuation = continuation; } • サンプルです。そのまま使わないでください。

58.

サンプル: HTTPリクエストして レスポンスのJSONをパースする

59.

コルーチンで書く IEnumerator Coroutine() { UnityWebRequest request = UnityWebRequest.Get("https://api.etherscan.io/api? module=proxy&action=eth_getBlockByNumber&tag=0x517df3&boolean=true&apikey=YourApiKeyToken"); yield return request.SendWebRequest(); 非同期 var text = request.downloadHandler.text; var data = (JObject)JsonConvert.DeserializeObject(text); var result = (JObject)data["result"]; var transactions = (JArray)result["transactions"]; Debug.Log(transactions.Count); } 同期

61.
[beta]
Task + async/awaitを使って書く
async Task Async() {
UnityWebRequest request = UnityWebRequest.Get("https://api.etherscan.io/api?
module=proxy&action=eth_getBlockByNumber&tag=0x517df3&boolean=true&apikey=YourApiKeyToken");
await request.SendWebRequest();

非同期

var text = request.downloadHandler.text;
await Task.Run(() => {
var data = (JObject)JsonConvert.DeserializeObject(text);
var result = (JObject)data["result"];
var transactions = (JArray)result["transactions"];
Debug.Log(JsonConvert.DeserializeObject(text));
});
}

非同期

63.

JSONパース処理(45ms)分が 非同期(メインスレッド外)で行われるように

64.

実はUnityのプロファイラーには Profiler.BeginThreadProfilingしたスレッドしか 表示されない

65.

スレッドプールのスレッドごとに Profiler.BeginThreadProfilingを呼ぶ TaskSchedulerを作ればよい 参考: https://github.com/tnayuki/Unity-AsyncAwait/blob/master/Assets/ UnityTaskScheduler.cs ※ 参照実装です。そのまま使わないでください。

66.

まとめ • async/awaitは非同期処理を待つための仕組み • 仕組みがわからない時は逆コンパイルしよう(ライセンスには気をつけよう) • コルーチン/C# Job Systemと使い分けよう • Task.Runに回した処理は通常はUnityのプロファイラーから見えないので注意

67.

「async/await完全に理解した」

68.

Thank you! ご静聴ありがとうございました