Closed12
More Effective C# 6.0/7.0 のメモ(第3章)
項目27 非同期処理には非同期メソッドを使おう
- await命令に到達すると、非同期メソッドはTaskオブジェクトをreturnする。このTaskオブジェクトは、非同期処理が完了しないことを示している。
- awaitで待っていたタスクが完了したら、awaitの次にある命令から実行を続ける。そのメソッドの残りの処理が完了したら、先ほど返したTaskオブジェクトを更新して、結果を入れる。
- そのTaskオブジェクトが、待っていたコードに完了を通知する。
- asyncメソッドの中で、「awaitが完了したらその次の行から実行する」のような機構は、SynchronizeationContextクラスによって実装されている。
非同期メソッドの例
public class Program
{
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
var awaitable = RunAsync();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync");
var result = await awaitable;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] done: {result}");
}
private static async Task<int> RunAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] starting");
await Task.Run(Count);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] done");
return 10;
}
private static void Count()
{
for (int i = 0; i < 1000; i++)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} Counting: {i}");
}
}
}
出力
1 [Main] start
1 [RunAsync] starting
4 Counting: 0
4 Counting: 1
...略...
4 Counting: 194
1 [Main] after RunAsync
4 Counting: 195
...略...
4 Counting: 998
4 Counting: 999
4 [RunAsync] done
4 [Main] done: 10
- 推論された型に注目
SharpLabで簡単な非同期処理がどのように解釈されるかを見る
public class Program
{
public static async Task Main()
{
var awaitable = RunAsync();
var result = await awaitable;
}
private static async Task<int> RunAsync()
{
await Task.Run(Count);
return 10;
}
private static void Count()
{
for (int i = 0; i < 1000; i++)
{
Console.WriteLine($"Counting: {i}");
}
}
}
- これをSharpLabにいれて変換された後
[NullableContext(1)]
[Nullable(0)]
public class Program
{
[CompilerGenerated]
private static class <>O
{
[Nullable(0)]
public static Action <0>__Count;
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <Main>d__0 : IAsyncStateMachine
{
public int <>1__state;
public AsyncTaskMethodBuilder <>t__builder;
[Nullable(0)]
private TaskAwaiter<int> <>u__1;
private void MoveNext()
{
int num = <>1__state;
try
{
TaskAwaiter<int> awaiter;
if (num != 0)
{
awaiter = RunAsync().GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter<int>);
num = (<>1__state = -1);
}
awaiter.GetResult();
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult();
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[StructLayout(LayoutKind.Auto)]
[CompilerGenerated]
private struct <RunAsync>d__1 : IAsyncStateMachine
{
public int <>1__state;
[Nullable(0)]
public AsyncTaskMethodBuilder<int> <>t__builder;
private TaskAwaiter <>u__1;
private void MoveNext()
{
int num = <>1__state;
int result;
try
{
TaskAwaiter awaiter;
if (num != 0)
{
awaiter = Task.Run(<>O.<0>__Count ?? (<>O.<0>__Count = new Action(Count))).GetAwaiter();
if (!awaiter.IsCompleted)
{
num = (<>1__state = 0);
<>u__1 = awaiter;
<>t__builder.AwaitUnsafeOnCompleted(ref awaiter, ref this);
return;
}
}
else
{
awaiter = <>u__1;
<>u__1 = default(TaskAwaiter);
num = (<>1__state = -1);
}
awaiter.GetResult();
result = 10;
}
catch (Exception exception)
{
<>1__state = -2;
<>t__builder.SetException(exception);
return;
}
<>1__state = -2;
<>t__builder.SetResult(result);
}
void IAsyncStateMachine.MoveNext()
{
//ILSpy generated this explicit interface implementation from .override directive in MoveNext
this.MoveNext();
}
[DebuggerHidden]
private void SetStateMachine(IAsyncStateMachine stateMachine)
{
<>t__builder.SetStateMachine(stateMachine);
}
void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine)
{
//ILSpy generated this explicit interface implementation from .override directive in SetStateMachine
this.SetStateMachine(stateMachine);
}
}
[AsyncStateMachine(typeof(<Main>d__0))]
public static Task Main()
{
<Main>d__0 stateMachine = default(<Main>d__0);
stateMachine.<>t__builder = AsyncTaskMethodBuilder.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
[AsyncStateMachine(typeof(<RunAsync>d__1))]
private static Task<int> RunAsync()
{
<RunAsync>d__1 stateMachine = default(<RunAsync>d__1);
stateMachine.<>t__builder = AsyncTaskMethodBuilder<int>.Create();
stateMachine.<>1__state = -1;
stateMachine.<>t__builder.Start(ref stateMachine);
return stateMachine.<>t__builder.Task;
}
private static void Count()
{
int num = 0;
while (num < 1000)
{
DefaultInterpolatedStringHandler defaultInterpolatedStringHandler = new DefaultInterpolatedStringHandler(10, 1);
defaultInterpolatedStringHandler.AppendLiteral("Counting: ");
defaultInterpolatedStringHandler.AppendFormatted(num);
Console.WriteLine(defaultInterpolatedStringHandler.ToStringAndClear());
num++;
}
}
}
項目28 async voidメソッドを書くべからず
- async voidメソッドをawaitすることはできない。そのため、async voidメソッドを呼び出すコードは、そのasyncメソッドから送出される例外をキャッチあるいは伝播する方法がない。
- 例外キャッチができないだけではなく、async voidメソッドはawaitすることができないため、タスクの実行完了を検知することができない。いつ終わるかわからない。
async voidとasync Taskのエラーハンドリング
async Taskの場合
public class Program
{
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
try
{
await RunAsync();
}
catch (Exception ex)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] caught exception: {ex.Message}");
}
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync");
}
private static async Task RunAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] starting");
await Task.Run(ThrowError);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] done");
}
private static void ThrowError()
{
throw new Exception("My Error");
}
}
出力
1 [Main] start
1 [RunAsync] starting
5 [Main] caught exception: My Error
5 [Main] after RunAsync
async voidの場合
- awaitを書くことができず、例外がキャッチできないため、エラーハンドリングができない。
- async voidメソッドの中で発生した例外は、そのasync voidメソッドが始動されたときにアクティブとなるSynchronizationContextに向けて直接送出される。この例外を処理することは困難となる。
public class Program
{
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
try
{
RunAsync(); // awaitできない Type 'void' is not awaitable
}
catch (Exception ex)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] caught exception: {ex.Message}");
}
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync");
}
private static async void RunAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] starting");
await Task.Run(ThrowError);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] done");
}
出力
1 [Main] start
1 [RunAsync] starting
1 [Main] after RunAsync
Unhandled exception. System.Exception: My Error
項目29 同期メソッドと非同期メソッドの混成を避けよう
- 同期メソッドを非同期メソッドの上に置くと、以下の点で良くない
- 例外処理が複雑になる
- デッドロックの可能性がある
- リソースを消費する
- 同期メソッドを非同期メソッドの上に置くというのは、awaitでタスクの完了を待つのではなく、Task.Wait()やTask.Resultでのアクセスを行うことでasyncのつかないメソッドの中でタスクを待つこと、で実現できる。
例外処理の複雑さの違い
- タスクをawaitした場合は、そのリストに例外が含まれていたら、最初の例外が送出される。
- Task.Wait()またはTask.Resultでアクセスした場合は、リストの内容を集積したAggregateExceptionが送出される。
- 特定のエラーのみをキャッチしたい時、AggregateExceptionの場合はそこからTypeが含まれていたら...などと条件を見る必要があり複雑になる。
同期・非同期メソッドでのエラーのキャッチの違い
同期メソッドの場合
- Task.Resultでアクセスした場合は、エラーリストの内容を修正したAggregateExceptionが送出される。
public class Program
{
// 同期メソッドとする
public static void Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
try
{
var result = RunAsync().Result;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync {result}");
}
catch (Exception ex)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] caught exception: {ex.GetType()}, {ex.Message}");
}
}
private static async Task<int> RunAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] starting");
await Task.Run(ThrowError);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [RunAsync] done");
return 10;
}
private static void ThrowError()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [ThrowError] starting");
throw new Exception("My Error");
}
}
出力
1 [Main] start
1 [RunAsync] starting
5 [ThrowError] starting
1 [Main] caught exception: System.AggregateException, One or more errors occurred. (My Error)
- AggregateExceptionが来ることを期待してハンドリングするコード
public static void Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
try
{
var result = RunAsync().Result;
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync {result}");
}
catch (AggregateException ex)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] caught exception: {ex.GetType()}, {ex.Message}");
foreach (var item in ex.InnerExceptions)
{
Console.WriteLine($"Inner Exception: {item.Message}");
}
}
}
出力
1 [Main] start
1 [RunAsync] starting
4 [ThrowError] starting
1 [Main] caught exception: System.AggregateException, One or more errors occurred. (My Error)
Inner Exception: My Error
非同期メソッドの場合
// 非同期メソッドとする
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] start");
try
{
var result = await RunAsync();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] after RunAsync {result}");
}
catch (Exception ex)
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] caught exception: {ex.GetType()}, {ex.Message}");
}
}
出力
1 [Main] start
1 [RunAsync] starting
5 [ThrowError] starting
5 [Main] caught exception: System.Exception, My Error
デッドロックについて
- コンソールアプリでは問題なくても、GUIまたはWeb(ASP.NET)では問題となるケースがある
- コンソールではマルチスレッドだが、GUIやASP.NETではシングルスレッドに入ってしまうから(ということだが実際に確認できていないので理解度が低い)
コード
awaitで待てば問題がない
public class Program
{
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
await DelayAsync();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static async Task DelayAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] DelayAsync Start");
await Task.Delay(3000);
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] DelayAsync End");
}
}
出力
1 [Main] Start
1 [Main] DelayAsync Start
4 [Main] DelayAsync End ← これが出るまで3秒待つ
4 [Main] End
同期なメソッドで待つとだめらしい
public static void Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
DelayAsync().Wait();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
出力
1 [Main] Start
1 [Main] DelayAsync Start
3 [Main] DelayAsync End ← これが出るまで3秒待つ
1 [Main] End
項目30 非同期処理に不要なスレッド割り当てを避けよう
- 非同期処理を書くと、その仕事を別のスレッドに投げることで、メインスレッドを開放することができる。
- しかし、開放されたメインスレッドでなにも他の仕事を実行することはなく、ただ投げた非同期処理の完了を待つだけの場合は、コンソールアプリケーションにおいては無益である。
- GUIアプリケーションの場合はUIの応答を維持するために必要
- スレッドの割り当てにはオーバーヘッドがかかるため、不必要な割り当ては避けたい(これはASP.NET限定の話?ちょっと掴みきれなかった)
項目31の前に、SynchronazationContextについて
SynchronizationContextの挙動について
コンソールアプリ / Unityで実験
コンソールアプリで実行
- コンソールアプリの場合、SynchronizationContext.Currentはnullとなっている。よってawaitの後にスレッドが戻ってこない
public class Program
{
public static async Task Main()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
Console.WriteLine($"[Main] SynchronizationContext.Current: {SynchronizationContext.Current}");
await MethodAsync();
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static async Task MethodAsync()
{
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Start");
Console.WriteLine($"[MethodAsync] SynchronizationContext.Current: {SynchronizationContext.Current}");
await Task.Run(() => Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"));
Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] End");
}
}
出力
1 [Main] Start
[Main] SynchronizationContext.Current:
1 [MethodAsync] Start
[MethodAsync] SynchronizationContext.Current:
8 [MethodAsync] Task.Run
8 [MethodAsync] End
8 [Main] End
Unityで実行
-
SynchronizationContext.Current
が、UnityEngine.UnitySynchronizationContext
と設定されている。 - よってawaitの後にスレッドが戻ってくる。
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
Debug.Log($"[Main] SynchronizationContext.Current: {SynchronizationContext.Current}");
await MethodAsync();
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static async Task MethodAsync()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Start");
Debug.Log($"[MethodAsync] SynchronizationContext.Current: {SynchronizationContext.Current}");
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"));
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] End");
}
}
出力
Unityでデッドロックを起こす
- 以下のコードを実行すると再生してすぐに固まって動かなくなる。
-
task.Wait();
によってメインスレッドがロックされる。awaitはメインスレッドに処理を返してタスク完了となる。 - メインスレッドはロックされているため、いつまで経ってもawaitは返却できず、したがってタスクは完了しない。永遠にWaitし続ける。
public class AsyncTest : MonoBehaviour
{
private void Start()
{
var task = MethodAsync();
task.Wait(); // ここでメインスレッドをロックする
// await task;とすれば問題ない。
}
private static async Task MethodAsync()
{
// awaitは、元のスレッドに処理を返して初めてタスク完了となる
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"));
}
}
.ConfigureAwait(false)でスレッドを戻さない設定とする(Unity)
- ConfigureAwait(false)をTaskに対して設定すると、awaitの後にスレッドを戻さなくなる。
ConfigureAwait(false)にしない通常の場合
using UnityEngine;
using System.Threading;
using System.Threading.Tasks;
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
Debug.Log($"[Main] SynchronizationContext.Current: {SynchronizationContext.Current}");
var task = MethodAsync();
await task;
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static async Task MethodAsync()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Start");
Debug.Log($"[MethodAsync] SynchronizationContext.Current: {SynchronizationContext.Current}");
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"));
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] End");
}
}
出力
- Task.Run以外は全てメインスレッドで実行されている。
出力
ConfigureAwait(false)の場合
// ログ出力は省略
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
var task = MethodAsync();
await task;
}
private static async Task MethodAsync()
{
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"))
.ConfigureAwait(false);
}
}
出力
- Task.Runを抜けた後も同じスレッドにいる
// ログ出力は省略
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
var task = MethodAsync();
await task.ConfigureAwait(false);
}
private static async Task MethodAsync()
{
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"))
.ConfigureAwait(false);
}
}
出力
- Task.Runの後に加えて、Startメソッドの最後もメインスレッドに戻っていない
task.Wait()を使ってもデッドロックを起こさない
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
var task = MethodAsync();
task.Wait();
}
private static async Task MethodAsync()
{
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"))
.ConfigureAwait(false);
}
}
出力
ContinueWithでTaskに続くスレッドを指定する(Unityで実験)
- await前のメインスレッドとは別のスレッドで実行可能
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
var task = MethodAsync();
await task;
}
private static async Task MethodAsync()
{
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"))
.ContinueWith(_ =>
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] ContinueWith");
},
TaskScheduler.Default
);
}
}
ContinueWithでメインスレッドで実行するには
// 重要ではないログは省略
public class AsyncTest : MonoBehaviour
{
private async void Start()
{
var task = MethodAsync();
await task;
}
private static async Task MethodAsync()
{
var currentSynchronizationContextScheduler = TaskScheduler.FromCurrentSynchronizationContext();
await Task.Run(() => Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] Task.Run"))
.ContinueWith(_ =>
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [MethodAsync] ContinueWith");
},
currentSynchronizationContextScheduler
);
}
}
Unityで、SynchronizationContextのPostを使ってみる
- まずは通常の場合
using UnityEngine;
using System.Threading;
public class AsyncTest : MonoBehaviour
{
private void Start()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
Debug.Log($"[Main] SynchronizationContext.Current: {SynchronizationContext.Current}");
ThreadPool.QueueUserWorkItem(_ =>
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [ThreadPool] start");
Thread.Sleep(100);
EndAction();
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [ThreadPool] end");
});
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static void EndAction()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [EndAction]");
}
}
出力
- ThreadPool内から呼ばれる処理は全てメインスレッドではない場所で実行されている
Postを使ってメインスレッドで実行させた場合
using UnityEngine;
using System.Threading;
public class AsyncTest : MonoBehaviour
{
private void Start()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] Start");
Debug.Log($"[Main] SynchronizationContext.Current: {SynchronizationContext.Current}");
var context = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [ThreadPool] start");
Thread.Sleep(100);
context.Post(_ => EndAction(), null);
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [ThreadPool] end");
});
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [Main] End");
}
private static void EndAction()
{
Debug.Log($"{Thread.CurrentThread.ManagedThreadId} [EndAction]");
}
}
出力
- EndActionがメインスレッドで実行されている
メインスレッドからのみContextが取得できる(Unity)
using UnityEngine;
using System.Threading;
public class AsyncTest : MonoBehaviour
{
private void Start()
{
Debug.Log($"[Main] : {SynchronizationContext.Current}");
ThreadPool.QueueUserWorkItem(_ =>
{
Debug.Log($"[ThreadPool]: {SynchronizationContext.Current}");
Thread.Sleep(100);
EndAction();
});
}
private static void EndAction()
{
Debug.Log($"[EndAction]: {SynchronizationContext.Current}");
}
}
using UnityEngine;
using System.Threading;
public class AsyncTest : MonoBehaviour
{
private void Start()
{
Debug.Log($"[Main] : {SynchronizationContext.Current}");
var context = SynchronizationContext.Current;
ThreadPool.QueueUserWorkItem(_ =>
{
Debug.Log($"[ThreadPool]: {SynchronizationContext.Current}");
Thread.Sleep(100);
context.Post(_ => EndAction(), null);
});
}
private static void EndAction()
{
Debug.Log($"[EndAction]: {SynchronizationContext.Current}");
}
}
項目31 不要なコンテキスト切り替えを避けよう
- awaitでは、明示的に
ConfigureAwait(false)
をTaskに付与しない限り、awaitの直前のコンテキスト(スレッド)をキャプチャし、awaitの後はキャプチャしたコンテキストに戻ろうとする- (CUIアプリはそうではないことがある)
- UIコンテキストと関わる場合などは、awaitの後の必ずUIコンテキストに処理を戻さなければアプリがクラッシュするケースがある。なのでデフォルトでは、awaitはコンテキストをキャプチャする。
- しかし、多くの場合はawaitの前のコンテキストに戻る必要がない。であれば、戻らなくて良い非同期メソッドは戻ることを強制しないほうが、リソースを効率的に利用できる。
- 確実に戻る必要があるメソッドと、そうではないメソッドを分離することで、その実装がしやすくなる。
- 同じメソッドの中だと、一度コンテキストからずれたらもう戻れないため。
項目32 複数のTaskオブジェクトで非同期処理を構成する
- 複数のTaskオブジェクトを利用する時、それぞれのタスクをどのように待つ必要があるかを考える。
- 1つ1つawaitして次へ、とするのが効率が悪いときはWhenAllなどを利用するとよい。
1つ1つタスクの完了を待つ場合
public class Program
{
public static async Task Main()
{
// 実行にかかる時間を計算する
var watch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, Start");
var symbols = new List<string> { "Apple", "Google", "Meta", "Amazon" };
var results = await ReadStackTicker(symbols);
foreach (var result in results)
{
Console.WriteLine(result);
}
watch.Stop();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, 処理時間: {watch.ElapsedMilliseconds} ms");
}
private static async Task<IEnumerable<string>> ReadStackTicker(IEnumerable<string> symbols)
{
var results = new List<string>();
var increment = 0;
foreach (var symbol in symbols)
{
// 1つ1つのタスクが完了したのを待ってから次のタスクに進む
var result = await ReadSymbol(symbol, increment);
results.Add(result);
increment++;
}
return results;
}
private static async Task<string> ReadSymbol(string symbol, int num)
{
await Task.Delay(1000);
var current = DateTime.Now;
return $"スレッド: {Thread.CurrentThread.ManagedThreadId}, {current.Second}:{current.Millisecond}, {symbol}_{num}";
}
}
出力
スレッド: 1, Start
スレッド: 5, 13:622, Apple_0
スレッド: 5, 14:854, Google_1
スレッド: 5, 15:855, Meta_2
スレッド: 5, 16:856, Amazon_3
スレッド: 5, 処理時間: 4248 ms
全てのタスクを並列で実行させた場合(WhenAll)
// 他は同じなので省略
private static async Task<IEnumerable<string>> ReadStackTicker(IEnumerable<string> symbols)
{
var resultTasks = new List<Task<string>>();
var increment = 0;
foreach (var symbol in symbols)
{
resultTasks.Add(ReadSymbol(symbol, increment));
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, タスク{increment}の追加完了直後");
increment++;
}
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, WhenAllの直前");
var results = await Task.WhenAll(resultTasks);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, WhenAllの直後");
return results;
}
出力
スレッド: 1, Start
スレッド: 1, 37:524, タスク0の追加完了直後
スレッド: 1, 37:524, タスク1の追加完了直後
スレッド: 1, 37:524, タスク2の追加完了直後
スレッド: 1, 37:524, タスク3の追加完了直後
スレッド: 1, 37:524, WhenAllの直前
スレッド: 11, 38:526, WhenAllの直後
スレッド: 8, 38:311, Apple_0
スレッド: 8, 38:525, Google_1
スレッド: 11, 38:526, Meta_2
スレッド: 8, 38:525, Amazon_3
スレッド: 11, 処理時間: 1225 ms
全てのタスクを並列で実行させて一番最初に完了したものを利用する場合(WhenAny)
public static async Task Main()
{
// 実行にかかる時間を計算する
var watch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, Start");
var symbols = new List<string> { "Apple", "Google", "Meta", "Amazon" };
var result = await ReadStackTicker(symbols);
Console.WriteLine(result);
watch.Stop();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, 処理時間: {watch.ElapsedMilliseconds} ms");
}
private static async Task<string> ReadStackTicker(IEnumerable<string> symbols)
{
var resultTasks = new List<Task<string>>();
var increment = 0;
foreach (var symbol in symbols)
{
resultTasks.Add(ReadSymbol(symbol, increment));
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, タスク{increment}の追加完了直後");
increment++;
}
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, WhenAnyの直前");
var result = await Task.WhenAny(resultTasks);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}:{DateTime.Now.Millisecond}, WhenAnyの直後");
return await result;
}
// 他は省略
出力
スレッド: 1, Start
スレッド: 1, 33:768, タスク0の追加完了直後
スレッド: 1, 33:768, タスク1の追加完了直後
スレッド: 1, 33:768, タスク2の追加完了直後
スレッド: 1, 33:768, タスク3の追加完了直後
スレッド: 1, 33:768, WhenAnyの直前
スレッド: 4, 34:548, WhenAnyの直後
スレッド: 4, 34:548, Apple_0
スレッド: 4, 処理時間: 1029 ms
- ちなみに、最後の
return await result;
の部分をreturn result;
とするとコンパイルエラーとなる。 Cannot convert expression type 'System.Threading.Tasks.Task<string>' to return type 'string'
- これは、Task.WhenAnyの戻り値が
Task<Task<string>>
であり、resultの形がTask<string>
となるため。public static Task<Task<TResult>> WhenAny<TResult>(IEnumerable<Task<TResult>> tasks)
- もう一度awaitしてstring型を取り出す必要がある。
タスクの完了後に継続して実行したいコードがあるとき(非効率なawait版)
- 以下のコードであれば、同時に始まったタスクでも、後のタスクほど待機時間が長くなるように設定している。そのため、追加のコードを実行するための待ち時間が前のタスクの完了を待つ、のような非効率的なことはたまたまない。
public class Program
{
public static async Task Main()
{
// 実行にかかる時間を計算する
var watch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, Start");
var symbols = new List<string> { "Apple", "Google", "Meta", "Amazon" };
var results = await ReadStackTicker(symbols);
foreach (var result in results)
{
Console.WriteLine(result);
}
watch.Stop();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, 処理時間: {watch.ElapsedMilliseconds} ms");
}
private static async Task<IEnumerable<string>> ReadStackTicker(IEnumerable<string> symbols)
{
var resultTasks = new List<Task<string>>();
var results = new List<string>();
var increment = 0;
foreach (var symbol in symbols)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, タスク{increment}の追加完了直前");
resultTasks.Add(ReadSymbol(symbol, increment));
increment++;
}
foreach (var task in resultTasks)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, タスクの待機開始");
var result = await task;
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, タスクの待機完了");
results.Add(result); // ← タスクの完了後に継続して実行したいコード
}
return results;
}
private static async Task<string> ReadSymbol(string symbol, int num)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, {symbol}_{num}の開始直後");
await Task.Delay((num + 1) * 2000); // 奥のタスクほど待機時間が長い
return $"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, {symbol}_{num}の完了時点";
}
}
出力
スレッド: 1, Start
スレッド: 1, 33, タスク0の追加完了直前
スレッド: 1, 33, Apple_0の開始直後
スレッド: 1, 33, タスク1の追加完了直前
スレッド: 1, 33, Google_1の開始直後
スレッド: 1, 33, タスク2の追加完了直前
スレッド: 1, 33, Meta_2の開始直後
スレッド: 1, 33, タスク3の追加完了直前
スレッド: 1, 33, Amazon_3の開始直後
スレッド: 1, 33, タスクの待機開始
スレッド: 8, 35, タスクの待機完了 <-- これが表示されるまで2秒待つ
スレッド: 8, 35, タスクの待機開始
スレッド: 8, 37, タスクの待機完了 <-- これが表示されるまで2秒待つ
スレッド: 8, 37, タスクの待機開始
スレッド: 11, 39, タスクの待機完了 <-- これが表示されるまで2秒待つ
スレッド: 11, 39, タスクの待機開始
スレッド: 11, 41, タスクの待機完了 <-- これが表示されるまで2秒待つ
スレッド: 8, 35, Apple_0の完了時点
スレッド: 8, 37, Google_1の完了時点
スレッド: 11, 39, Meta_2の完了時点
スレッド: 11, 41, Amazon_3の完了時点
スレッド: 11, 処理時間: 8228 ms
- しかし、待機時間の部分を後のタスクほど短くなるようにすると、「自分のタスクは完了して完了後の継続コードを実行したいのに、前のタスクが完了していないため待つ必要がある」という非効率なことになる。
await Task.Delay((4 - num) * 2000); // 8, 6, 4, 2秒の待機
スレッド: 1, Start
スレッド: 1, 34, タスク0の追加完了直前
スレッド: 1, 34, Apple_0の開始直後
スレッド: 1, 34, タスク1の追加完了直前
スレッド: 1, 34, Google_1の開始直後
スレッド: 1, 34, タスク2の追加完了直前
スレッド: 1, 34, Meta_2の開始直後
スレッド: 1, 34, タスク3の追加完了直前
スレッド: 1, 34, Amazon_3の開始直後
スレッド: 1, 34, タスクの待機開始
スレッド: 5, 42, タスクの待機完了 <-- ここまで待ってから一気に出力
スレッド: 5, 42, タスクの待機開始
スレッド: 5, 42, タスクの待機完了
スレッド: 5, 42, タスクの待機開始
スレッド: 5, 42, タスクの待機完了
スレッド: 5, 42, タスクの待機開始
スレッド: 5, 42, タスクの待機完了
スレッド: 5, 42, Apple_0の完了時点
スレッド: 5, 40, Google_1の完了時点
スレッド: 5, 38, Meta_2の完了時点
スレッド: 5, 36, Amazon_3の完了時点
スレッド: 5, 処理時間: 8234 ms
TaskCompletionSourceを使ってTaskオブジェクトをあとから操作する
public static class Program
{
public static async Task Main()
{
// 実行にかかる時間を計算する
var watch = System.Diagnostics.Stopwatch.StartNew();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, Start");
var symbols = new List<int> { 2, 4, 3, 1, 5 };
var results = await ReadStackTicker(symbols);
foreach (var result in results)
{
Console.WriteLine(result);
}
watch.Stop();
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, 処理時間: {watch.ElapsedMilliseconds} ms");
}
private static async Task<IEnumerable<string>> ReadStackTicker(IEnumerable<int> symbols)
{
var resultTasks = new List<Task<string>>();
foreach (var symbol in symbols)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, {symbol}秒待つタスクの追加直前");
resultTasks.Add(ReadSymbol(symbol));
}
var orderedTasks = resultTasks.OrderByCompletion(); // すぐに返ってくる
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, OrderByCompletionの呼び出し直後");
var results = await Task.WhenAll(orderedTasks); // ここで待つ(全てのTaskにResultが入るまで)
return results;
}
private static async Task<string> ReadSymbol(int symbol)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, {symbol}秒待つタスクの呼び出し開始");
await Task.Delay(symbol * 1000);
return $"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, {symbol}秒待つタスクの完了";
}
}
public static class MyTaskExtension
{
public static Task<T>[] OrderByCompletion<T>(this IEnumerable<Task<T>> tasks)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, OrderByCompletionの中の1行目");
var sourceTasks = tasks.ToList();
var completionSources = new TaskCompletionSource<T>[sourceTasks.Count]; // 空の配列
var outputTasks = new Task<T>[completionSources.Length]; // 空の配列
for (int i = 0; i < completionSources.Length; i++)
{
completionSources[i] = new TaskCompletionSource<T>(); // 空の配列にctsの中身をいれる
outputTasks[i] = completionSources[i].Task; // outputの方にtcsをTaskにしたものを入れる(後でtcsに結果を入れられることを期待)
}
int nextTaskIndex = -1;
Action<Task<T>> continuation = completed =>
{
var bucket = completionSources[Interlocked.Increment(ref nextTaskIndex)];
if (completed.IsCompleted)
{
bucket.TrySetResult(completed.Result); // これによりoutputTasksに結果が入る
}
else if (completed.IsFaulted)
{
bucket.TrySetException(completed.Exception);
}
};
foreach (var inputTask in sourceTasks)
{
inputTask.ContinueWith(continuation,
CancellationToken.None,
TaskContinuationOptions.ExecuteSynchronously,
TaskScheduler.Default);
}
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, OrderByCompletionのreturn直前");
return outputTasks;
}
}
出力
スレッド: 1, Start
スレッド: 1, 49, 2秒待つタスクの追加直前
スレッド: 1, 49, 2秒待つタスクの呼び出し開始
スレッド: 1, 49, 4秒待つタスクの追加直前
スレッド: 1, 49, 4秒待つタスクの呼び出し開始
スレッド: 1, 49, 3秒待つタスクの追加直前
スレッド: 1, 49, 3秒待つタスクの呼び出し開始
スレッド: 1, 49, 1秒待つタスクの追加直前
スレッド: 1, 49, 1秒待つタスクの呼び出し開始
スレッド: 1, 49, 5秒待つタスクの追加直前
スレッド: 1, 49, 5秒待つタスクの呼び出し開始
スレッド: 1, 49, OrderByCompletionの中の1行目
スレッド: 1, 49, OrderByCompletionのreturn直前
スレッド: 1, 49, OrderByCompletionの呼び出し直後
スレッド: 5, 50, 1秒待つタスクの完了 <-- 5秒待ってここから一気に出力される
スレッド: 5, 51, 2秒待つタスクの完了
スレッド: 5, 52, 3秒待つタスクの完了
スレッド: 5, 53, 4秒待つタスクの完了
スレッド: 11, 54, 5秒待つタスクの完了
スレッド: 11, 処理時間: 5276 ms
脱線 TaskCompletionSourceについて
実験コード
public class Program
{
public static void Main()
{
Console.WriteLine("Main Start");
var tcs = new TaskCompletionSource<int>();
var task = tcs.Task;
var task2 = SampleTask(tcs);
Console.WriteLine(task.Result);
}
private static async Task SampleTask(TaskCompletionSource<int> tcs)
{
await Task.Delay(4000);
tcs.SetResult(1234);
}
}
出力
Main Start
1234 <-- 4秒後に出力
脱線 CancellationTokenSourceについて
デフォルトである非同期APIに対してtokenを渡す場合
public class Program
{
public static async Task Main()
{
var cts = new CancellationTokenSource();
var token = cts.Token;
// CancelTask(cts);
await CancelableTaskAsync(token);
}
private static async Task CancelableTaskAsync(CancellationToken token)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelableTaskAsync] Start");
await Task.Delay(5000, token);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelableTaskAsync] Completed");
}
private static async Task CancelTask(CancellationTokenSource cts)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelTask] Start");
await Task.Delay(2000);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelTask] Cancel");
cts.Cancel();
}
}
- まずキャンセルしない場合の出力
スレッド: 1, 51, [CancelableTaskAsync] Start
スレッド: 5, 56, [CancelableTaskAsync] Completed
- キャンセルする場合の出力
スレッド: 1, 30, [CancelTask] Start
スレッド: 1, 30, [CancelableTaskAsync] Start
スレッド: 8, 32, [CancelTask] Cancel
Unhandled exception. System.Threading.Tasks.TaskCanceledException: A task was canceled.
自分で途中でキャンセルする機構を作る場合
// 他は上と同じ
private static async Task CancelableTaskAsync(CancellationToken token)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelableTaskAsync] Start");
await Task.Delay(3000);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelableTaskAsync] 3000秒待った直後");
token.ThrowIfCancellationRequested();
await Task.Delay(2000);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [CancelableTaskAsync] Completed");
}
- キャンセルしない場合
スレッド: 1, 45, [CancelableTaskAsync] Start
スレッド: 5, 49, [CancelableTaskAsync] 3000秒待った直後
スレッド: 5, 51, [CancelableTaskAsync] Completed
- キャンセルする場合
-
cts.Cancel()
は2000秒待った時点で発火する。 - tokenが渡されているメソッドでは、3000秒を数えた後で、2000秒の時点で発火していたtokenにたどり着きキャンセルとなる。
-
スレッド: 1, 4, [CancelTask] Start
スレッド: 1, 5, [CancelableTaskAsync] Start
スレッド: 5, 7, [CancelTask] Cancel
スレッド: 5, 8, [CancelableTaskAsync] 3000秒待った直後
Unhandled exception. System.OperationCanceledException: The operation was canceled.
項目33 タスクのキャンセルや進捗報告を行うプロトコルを実装する
- いくつかの非同期タスクが組み合わさったような大きいタスクのとき、個別タスクの完了時に現在の進捗状況を報告するような機構を作ると便利
- 同様に、途中でキャンセルして問題がないのであればキャンセルを実現できるようにするのもよい。
- 進捗報告オプションがある場合、CancellationTokenが渡されている場合それぞれについてオーバーロードを作成するとよい。
項目34 総称的なValueTask<T>型で非同期処理の戻り値をキャッシュする
- メモリをかなり気にする状況では、Task型のインスタンスを作ることでマネージドヒープが確保されることを防ぎたい場合がある。
- ValueTaskは値型なのでその点で有効である。
ValueTaskのサンプルコード
public class Program
{
public static async Task Main()
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [Main] 開始");
var result = await ValueTaskSample(false);
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [Main] 結果: {result}");
}
private static async ValueTask<string> ValueTaskSample(bool isCached)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [ValueTaskSample] 開始");
if (isCached)
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [ValueTaskSample] キャッシュ値を返却");
return "Cached value";
}
else
{
Console.WriteLine($"スレッド: {Thread.CurrentThread.ManagedThreadId}, {DateTime.Now.Second}, [ValueTaskSample] 新しい値を返却");
await Task.Delay(1000);
return "New value";
}
}
}
-
await ValueTaskSample(false);
のとき(非同期処理となるとき)の出力
スレッド: 1, 18, [Main] 開始
スレッド: 1, 19, [ValueTaskSample] 開始
スレッド: 1, 19, [ValueTaskSample] 新しい値を返却
スレッド: 8, 20, [Main] 結果: New value
-
await ValueTaskSample(true);
のとき(キャッシュを利用することで非同期処理とならないとき)の出力
スレッド: 1, 31, [Main] 開始
スレッド: 1, 32, [ValueTaskSample] 開始
スレッド: 1, 32, [ValueTaskSample] キャッシュ値を返却
スレッド: 1, 32, [Main] 結果: Cached value
このスクラップは2024/04/21にクローズされました