[C# 5.0] async/await についての勉強メモ

勉強メモというか参考リンクが全て

参考リンク

以降は自分のメモなのであまり読む価値はないかも。つまずいたところなど。

メモ

  • async付きメソッドは中でawaitを使えるようになる。
  • async付きメソッドは中でawaitを使わなければ無意味。 つまりasyncを付けたから非同期になるわけじゃない。
  • awaitしても対象のTaskが完了状態の場合はそのawaitは非同期にならない。
  • async付きメソッドは一般的に Async というサフィックスを名前に付ける
  • async付きメソッドはawaitが出てくるまでは普通の関数と変わりはない。 awaitが出てきたところで非同期処理が実行され、呼び出し元に処理は戻る。
  • awaitの後続処理はどのスレッドで実行されるかは不定…だと思ったけど違った。 SynchronizationContextにより挙動が左右される大きな罠。
  • Task.Run(action)Task.Factory.StartNew(action, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default) のshorthand
  • 覚えておくとよさげなクラスやメソッド
    • Task.WhenAll: Task.WaitAll はブロックするのでダメ
    • Concurrentコレクション: BlockingCollection, ConcurrentDictionary, ConcurrentQueue, ConcurrentStack
    • Task.Delay: Thread.Sleepはダメ
    • PLINQのためのAsParallel, AsOrdered, WithDegreeOfParallelism

Task.Run(...)var t = new Task(...); t.Start() の違い

Taskのコンストラクタで生成したタスクは async void 関数と同様だと思えばよい。 つまり最初の await でそのタスクは完了状態となる。

Task.Runの戻り値のタスクは async Task であり、 これは非同期処理が全て完了したときに完了状態となる。 await があってもタスクは完了状態とならない。

参考: c# - Task.Start strange behavoir - Stack Overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main(string[] args)
{

var stopWath = Stopwatch.StartNew();
SubAsync().Wait();
Console.WriteLine("elapsed time: {1}", Thread.CurrentThread.ManagedThreadId, stopWatch.Elapsed);
Console.ReadLine();
}

static async Task SubAsync()
{

Console.WriteLine("BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

// async void は await で待てない
// (Task返ってこないので完了がわからない)ためコンパイルエラーになる
await VoidAsync();

//
// しかし・・・
//

// TaskにはFunc<Task>を受け取るコンストラクタがない。
// つまりTaskのコンストラクタで生成したタスクは`async void`扱いである。
var task = new Task(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
});
task.Start();

// これはコンパイルエラーにならない。
// 実際に実行してみるとTaskのコンストラクタに指定したAction内のawaitのところで
// task は完了状態となり1秒待たずに即座に次のコードが実行される。
await task;
Console.WriteLine("END. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
}

static async void VoidAsync()
{

await Task.Delay(1);
}
}

テスト1: コンソールアプリケーションでasync/await

こんなコンソールアプリケーションを実行してみる。 コンソールアプリケーションでは SynchronizationContext.Current が null なので注意。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
using System;
using System.Threading;
using System.Threading.Tasks;

class Program
{
static void Main(string[] args)
{

Console.WriteLine("Main BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
Program.SubAsync();
Console.WriteLine("Main END. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);

Console.ReadLine();
}

static async void SubAsync()
{

Console.WriteLine("Sub BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
await Task.Run(async () =>
{
Console.WriteLine("Delay BEGIN. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
await Task.Delay(TimeSpan.FromSeconds(1));
Console.WriteLine("Delay END. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
});
Console.WriteLine("Sub END. ThreadId={0}", Thread.CurrentThread.ManagedThreadId);
}
}

結果の一例(ThreadIdは実行毎に変わる。”Main END”と”Delay BEGIN”は実行順が入れ替わることあり)

1
Main  BEGIN. ThreadId=10
Sub   BEGIN. ThreadId=10
Delay BEGIN. ThreadId=11
Main  END.   ThreadId=10
Delay END.   ThreadId=6
Sub   END.   ThreadId=6

つまり

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Program
{
static void Main(string[] args)
{

1】Console.WriteLine("Main BEGIN. ThreadId=メインスレッド");
// await していないのでSubAsyncの非同期処理の終わりを待たない
Program.SubAsync();
3 or 4】Console.WriteLine("Main END. ThreadId=メインスレッド");

Console.ReadLine();
}

static async Task SubAsync()
{

2】Console.WriteLine("Sub BEGIN. ThreadId=メインスレッド");
await Task.Run(async () =>
{
3 or 4】Console.WriteLine("Delay BEGIN. ThreadId=非同期スレッド1");
await Task.Delay(TimeSpan.FromSeconds(1));
5】Console.WriteLine("Delay END. ThreadId=非同期スレッド2");
});

// あまり気にする意味はないと思うけど
// 非同期スレッド2と3は何度実行しても同じになった
6】Console.WriteLine("Sub END. ThreadId=非同期スレッド3");
}
}

テスト2: 完了したtaskをawaitしても非同期処理にならない

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Program
{
static void Main(string[] args)
{

SubAsync();
Console.ReadLine();
}

static async Task SubAsync()
{

int threadId = Thread.CurrentThread.ManagedThreadId;
var task = Task.Delay(TimeSpan.FromSeconds(1));

Thread.Sleep(TimeSpan.FromSeconds(3));

// "task is completed" となるはず
Console.WriteLine("task is {0} completed",
task.IsCompleted ? "" : "not");
await task;

// 前後でスレッドIDは変わらないはず
Console.WriteLine("await前後のスレッドID: {0} - {1}",
threadId, Thread.CurrentThread.ManagedThreadId);
}
}