Unity C#で非同期処理にTaskとasync/awaitを使っている人向けです。
UniRxを使わずに、バックグラウンドスレッドからメインスレッドへの切り替えをやりやすく考えてみました。
単にメインスレッドで実行するだけなら
SynchronizationContext
を使うことで、メインスレッドに処理を戻すこと自体はとても簡単にできます。
// メインスレッドで実行しておく
var mainContext = SynchronizationContext.Current;
// バックグラウンドスレッドで以下のように実行
// asynchronously
mainContext.Post((_) =>
{
// ここにメインスレッドで実行したい処理を記述
}, null);
// synchronously
mainContext.Send((_) =>
{
// ここにメインスレッドで実行したい処理を記述
}, null);
Unityでは自動的にSynchronizationContextのSetが行われているので、メインスレッド上でSynchronizationContext.Current
を参照するだけでメインスレッドのコンテキストを取得することができます。これをあらかじめ取得しておくことで、バックグラウンドスレッドからでもメインスレッドで処理するように指定することができます。
この場合、Postメソッドのcallbackは非同期的に、Sendメソッドのcallbackは同期的に実行されます。
callbackがasyncの場合
上記だけでも簡単にメインスレッドに戻すことができて、これが追加パッケージ無しで使えるというのは素晴らしいことなのですが、SynchronizationContext.Post(Send)
メソッドはvoidなのでcallbackからの返り値を受け取ることができません。
メインスレッドでcallbackを実行しつつawaitして完了を待機できるし返り値を受け取ることもできる、ようにしたいですね。
async Task SomeBackgroundTask()
{
// メソッド内は基本的にバックグラウンドスレッドでやりたい処理
// callbackに渡した処理だけはメインスレッド上で実行したい
var res = await MainThreadDispatcher.RunAsync(async () => {
// ここにメインスレッドで実行したい処理
})
// メインスレッドでの処理が完了してから実行したい処理
}
そこで、以下のように作ってみました。クラス名はUniRxからパクリ。
public static class MainThreadDispatcher
{
private static SynchronizationContext _mainThreadContext;
public static void SetMainThreadContext()
{
var current = SynchronizationContext.Current;
_mainThreadContext = current ?? throw new InvalidOperationException();
}
// メインスレッドでアクションを実行
public static void Post(Action action)
{
if (_mainThreadContext == null)
throw new InvalidOperationException();
_mainThreadContext.Post(_ => action(), null);
}
public static Task<TResult> RunAsync<TResult>(Func<Task<TResult>> func)
{
var tcs = new TaskCompletionSource<TResult>();
Post(async () =>
{
try
{
var res = await func();
tcs.SetResult(res);
}
catch (Exception ex)
{
tcs.SetException(ex);
}
});
return tcs.Task;
}
}
以下は動作サンプルです。
Task.Runのcallback内では自動的にバックグラウンドスレッドに切り替わって処理が実行されますが、MainThreadDispatcher.RunAsync
のcallbackではしっかりメインスレッド(ThreadId: 1)に処理を戻すことができています。
また、RunAsyncのcallbackが最終的に返したCompleted
をresで受け取ることができていますね。
private void OnEnable()
{
// メインスレッドで予め呼び出しておく
MainThreadDispatcher.SetMainThreadContext();
}
void Start()
{
Task.Run(async () =>
{
Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
var res = await MainThreadDispatcher.RunAsync(async () =>
{
// ここに UI スレッドで実行したい処理を記述
Debug.Log($"This Run on the UI thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
return "Completed";
});
Debug.Log($"res = {res}");
Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
});
}
/** 出力
This Run on the background thread. ThreadId: 52
This Run on the UI thread. ThreadId: 1
~~~ 1秒待機 ~~~
res = Completed
This Run on the background thread. ThreadId: 95
*/
これだけですべてのパターンに対応できるわけではありませんが、メインスレッド上で指定のasyncメソッドを実行し、さらにそれをawaitして結果を受け取ることができるようになりました!
実際のところ
プロジェクトの都合上必要だったので作成してはみたのですが、バックグラウンドスレッドで実行すべき処理だけをTask.Runすれば、上記の動作サンプルではこのクラスは不要ですね。本当に必要な時を見極める必要はあるかもしれません。
async void Start()
{
// StartはUIスレッドで実行される
// awaitでcallbackの完了まで待機可能
await Task.Run(() =>
{
// Task.Runのcallbackはバックグラウンドスレッド
Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
});
// ここはUIスレッドで実行される
Debug.Log($"This Run on the UI thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
await Task.Delay(1000);
var res = "Completed";
await Task.Run(() => {
Debug.Log($"res = {res}");
Debug.Log($"This Run on the background thread. ThreadId: {Thread.CurrentThread.ManagedThreadId}");
});
}
以上です。