7.4K Views
May 09, 18
スライド概要
講演者:名雪 通(ユニティ・テクノロジーズ・ジャパン合同会社)
:安原 祐二(ユニティ・テクノロジーズ・ジャパン合同会社)
こんな人におすすめ
・C#プログラマー
・.NETで開発経験があり、これからUnityを使い始めるプログラマー
・「async/await完全に理解した」と言いたい人
受講者が得られる知見
・async/awaitの知識
リアルタイム3Dコンテンツを制作・運用するための世界的にリードするプラットフォームである「Unity」の日本国内における販売、サポート、コミュニティ活動、研究開発、教育支援を行っています。ゲーム開発者からアーティスト、建築家、自動車デザイナー、映画製作者など、さまざまなクリエイターがUnityを使い想像力を発揮しています。
2018/05/07 - 09 さては非同期だなオメー! async/await完全に理解しよう 名雪 通 ユニティ・テクノロジーズ・ジャパン
名雪 通 エンジニア asyncのほう。
安原 祐二 エンジニア awaitのほう。
#unitetokyo2018 #完全に理解した
2018年5月
Unity 2018.1 リリース
.NET 4.xがExperimental→Stableに!
C# 4.0→6.0
参考: C# 5.0の新機能 • async/await • 呼び出し元情報属性
参考: C# 6.0の新機能 • 読み取り専用の自動プロパティ • 自動プロパティの初期化子 • 式形式の関数メンバー • using static • null条件演算子 • 文字列補間 • 例外フィルター • nameof式 • catchブロックとfinallyブロックでのawait • インデックス初期化子 • コレクション初期化子の拡張メソッド • オーバーロード解決の改善
async/await
async/awaitとは何か
async(= asynchronous = 非同期) awaitを使うメソッドにつける必要があるキーワード
await(= 待つ) 非同期(async)メソッドを呼び出し、 その完了まで実行を中断するキーワード
ここまででのasync/await理解率: 10%
ここで問題です
このコードの開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); Thread.Sleep(5000); // 終了 Console.WriteLine(DateTime.Now); }
普通に5秒
次の問題
このコードは開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); AsyncMethod(); // 終了 Console.WriteLine(DateTime.Now); } static async void AsyncMethod() { Thread.Sleep(5000); }
やっぱり5秒
別にasyncをつけただけで非同期になるわけではない
次はawaitの問題
このコードは開始→終了まで何秒かかるでしょうか static void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }
なんと0秒
次の問題
このコードは開始→終了まで何秒かかるでしょうか static async void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); await Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }
5秒になった!
async/awaitは 「処理を非同期に行うための仕組み」ではなく、 「非同期の処理を待つための仕組み」である 理解率: 50%
では、どのように その「非同期の処理を待つための仕組み」を 実現しているのか?
逆コンパイルしてみよう
さっきのコード static async void AsyncTest() { // 開始 Console.WriteLine(DateTime.Now); await Task.Delay(5000); // 終了 Console.WriteLine(DateTime.Now); }
逆コンパイルした結果
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__);
}
インナークラスも生成されている
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();
}
}
わかりやすく書き直してみる
元の関数 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); // 終了 }
インナークラス
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();
}
}
インナークラスの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() {
}
}
インナークラス
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);
関数を抜けてる!
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();
}
}
問題です
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); }
違う
同じコードをUnityで実行すると? async void Start () { // 前 Debug.Log(Thread.CurrentThread.ManagedThreadId); await Task.Delay(5000); // 後 Debug.Log(Thread.CurrentThread.ManagedThreadId); }
同じ!
SynchronizationContextが await後のコードを実行するスレッドを決める CLIアプリケーションには SynchronizationContextが存在しないため 自動的にスレッドプール上で実行される
UnityにはUnityの SynchronizationContextの実装がある 参考: https://github.com/Unity-Technologies/UnityCsReference/blob/ 83cceb769a97e24025616acc7503e9c21891f0f1/Runtime/Export/ UnitySynchronizationContext.cs
もともとUnityには 非同期処理のための仕組みがあります
コルーチン • C# 2.0の反復子(yield)を利用した継続処理の仕組み • 様々なタイミングを待つことができる(Unity側の仕組みによる) • フレームの終わり • FixedUpdate • 指定した秒数後(の最初のフレーム) • その他非同期処理(AsyncOperation) • 戻り値を返しにくい
コルーチンの例 IEnumerator Coroutine() { // 前 Debug.Log(Thread.CurrentThread.ManagedThreadId); yield return new WaitForSeconds(0.5f); // 後 Debug.Log(Thread.CurrentThread.ManagedThreadId); }
逆コンパイルしてみよう
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)
C# Job System • 2018.1の新機能 • クラスが使えない • Burstコンパイラーによる最適化の恩恵を受けられる • 処理はUnityのワーカースレッドで実行される
awaitできるのはTaskだけではない
TaskをawaitするのがTaskAwaiter Awaiterを作ればTaskでなくてもawaitできる
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; } • サンプルです。そのまま使わないでください。
サンプル: HTTPリクエストして レスポンスのJSONをパースする
コルーチンで書く 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); } 同期
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));
});
}
非同期
JSONパース処理(45ms)分が 非同期(メインスレッド外)で行われるように
実はUnityのプロファイラーには Profiler.BeginThreadProfilingしたスレッドしか 表示されない
スレッドプールのスレッドごとに Profiler.BeginThreadProfilingを呼ぶ TaskSchedulerを作ればよい 参考: https://github.com/tnayuki/Unity-AsyncAwait/blob/master/Assets/ UnityTaskScheduler.cs ※ 参照実装です。そのまま使わないでください。
まとめ • async/awaitは非同期処理を待つための仕組み • 仕組みがわからない時は逆コンパイルしよう(ライセンスには気をつけよう) • コルーチン/C# Job Systemと使い分けよう • Task.Runに回した処理は通常はUnityのプロファイラーから見えないので注意
「async/await完全に理解した」
Thank you! ご静聴ありがとうございました