簡単にするために、重い操作を実行しているときにオブジェクトを返す必要があるメソッドがあると想像してみましょう。実装する方法は2つあります。
_public Task<object> Foo()
{
return Task.Run(() =>
{
// some heavy synchronous stuff.
return new object();
}
}
_
そして
_public async Task<object> Foo()
{
return await Task.Run(() =>
{
// some heavy stuff
return new object();
}
}
_
生成されたILを調べた後、生成された2つの完全に異なるものがあります。
_.method public hidebysig
instance class [mscorlib]System.Threading.Tasks.Task`1<object> Foo () cil managed
{
// Method begins at RVA 0x2050
// Code size 42 (0x2a)
.maxstack 2
.locals init (
[0] class [mscorlib]System.Threading.Tasks.Task`1<object>
)
IL_0000: nop
IL_0001: ldsfld class [mscorlib]System.Func`1<object> AsyncTest.Class1/'<>c'::'<>9__0_0'
IL_0006: dup
IL_0007: brtrue.s IL_0020
IL_0009: pop
IL_000a: ldsfld class AsyncTest.Class1/'<>c' AsyncTest.Class1/'<>c'::'<>9'
IL_000f: ldftn instance object AsyncTest.Class1/'<>c'::'<Foo>b__0_0'()
IL_0015: newobj instance void class [mscorlib]System.Func`1<object>::.ctor(object, native int)
IL_001a: dup
IL_001b: stsfld class [mscorlib]System.Func`1<object> AsyncTest.Class1/'<>c'::'<>9__0_0'
IL_0020: call class [mscorlib]System.Threading.Tasks.Task`1<!!0> [mscorlib]System.Threading.Tasks.Task::Run<object>(class [mscorlib]System.Func`1<!!0>)
IL_0025: stloc.0
IL_0026: br.s IL_0028
IL_0028: ldloc.0
IL_0029: ret
}
_
そして
_.method public hidebysig
instance class [mscorlib]System.Threading.Tasks.Task`1<object> Foo () cil managed
{
.custom instance void [mscorlib]System.Runtime.CompilerServices.AsyncStateMachineAttribute::.ctor(class [mscorlib]System.Type) = (
01 00 1a 41 73 79 6e 63 54 65 73 74 2e 43 6c 61
73 73 31 2b 3c 42 61 72 3e 64 5f 5f 31 00 00
)
.custom instance void [mscorlib]System.Diagnostics.DebuggerStepThroughAttribute::.ctor() = (
01 00 00 00
)
// Method begins at RVA 0x2088
// Code size 59 (0x3b)
.maxstack 2
.locals init (
[0] class AsyncTest.Class1/'<Foo>d__1',
[1] valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>
)
IL_0000: newobj instance void AsyncTest.Class1/'<Foo>d__1'::.ctor()
IL_0005: stloc.0
IL_0006: ldloc.0
IL_0007: ldarg.0
IL_0008: stfld class AsyncTest.Class1 AsyncTest.Class1/'<Foo>d__1'::'<>4__this'
IL_000d: ldloc.0
IL_000e: call valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::Create()
IL_0013: stfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/'<Foo>d__1'::'<>t__builder'
IL_0018: ldloc.0
IL_0019: ldc.i4.m1
IL_001a: stfld int32 AsyncTest.Class1/'<Foo>d__1'::'<>1__state'
IL_001f: ldloc.0
IL_0020: ldfld valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/'<Foo>d__1'::'<>t__builder'
IL_0025: stloc.1
IL_0026: ldloca.s 1
IL_0028: ldloca.s 0
IL_002a: call instance void valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::Start<class AsyncTest.Class1/'<Foo>d__1'>(!!0&)
IL_002f: ldloc.0
IL_0030: ldflda valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object> AsyncTest.Class1/'<Foo>d__1'::'<>t__builder'
IL_0035: call instance class [mscorlib]System.Threading.Tasks.Task`1<!0> valuetype [mscorlib]System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1<object>::get_Task()
IL_003a: ret
}
_
最初のケースでわかるように、ロジックは単純です。ラムダ関数が作成され、_Task.Run
_の呼び出しが生成され、結果が返されます。 2番目の例では、AsyncTaskMethodBuilder
のインスタンスが作成され、タスクが実際にビルドされて返されます。 fooメソッドがより高いレベルでawait Foo()
として呼び出されることを常に期待していたので、私は常に最初の例を使用しました。しかし、私は後者をより頻繁に見ます。では、どちらのアプローチが正しいのでしょうか?それぞれにどのような長所と短所がありますか?
次のようなWebAPIコントローラー内で使用されるメソッドTask<User> GetUserByNameAsync(string userName)
を持つUserStore
があるとします。
_public async Task<IHttpActionResult> FindUser(string userName)
{
var user = await _userStore.GetUserByNameAsync(userName);
if (user == null)
{
return NotFound();
}
return Ok(user);
}
_
Task<User> GetUserByNameAsync(string userName)
のどの実装が正しいでしょうか?
_public Task<User> GetUserByNameAsync(string userName)
{
return _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == userName);
}
_
または
_public async Task<User> GetUserNameAsync(string userName)
{
return await _dbContext.Users.FirstOrDefaultAsync(user => user.UserName == username);
}
_
ILからわかるように、_async/await
_は、些細な非同期末尾呼び出しの場合でも、ステートマシン(および追加のTask
)を作成します。
_return await Task.Run(...);
_
これにより、追加の命令と割り当てが原因でパフォーマンスが低下します。したがって、経験則は次のとおりです。メソッドが_await ...
_または_return await ...
_で終わり、それが唯一のawait
である場合ステートメントの場合、一般的にasync
キーワードを削除して、待機する予定のTask
を直接返すのが安全です。
これを行うことの潜在的に意図しない結果の1つは、返されたTask
内で例外がスローされた場合、外部メソッドがスタックトレースに表示されないことです。
ただし、_return await ...
_の場合にも隠れた落とし穴があります。待機者がConfigureAwait(false)
を介してキャプチャされたコンテキストで続行するように明示的に構成されていない場合not、外側のTask
(非同期状態マシンによって作成されたもの) SynchronizationContext
への最後のポストバック(await
の直前にキャプチャされた)が終了するまで、完了状態に移行できません。これは実際の目的には役立ちませんが、何らかの理由で外部タスクをブロックすると、デッドロックが発生する可能性があります( 詳細な説明は次のとおりです そのような場合に何が起こるか)。
では、どちらのアプローチが正しいのでしょうか?
どちらでもない。
同期作業がある場合は、APIは同期する必要があります:
public object Foo()
{
// some heavy synchronous stuff.
return new object();
}
呼び出し元のメソッドがそのスレッドをブロックできる場合(つまり、ASP.NET呼び出しであるか、スレッドプールスレッドで実行されている場合)、呼び出し元のメソッドは直接呼び出します。
var result = Foo();
また、呼び出し元のスレッドがそのスレッドをブロックできない場合(つまり、UIスレッドで実行されている場合)、スレッドプールでFoo
を実行できます。
var result = await Task.Run(() => Foo());
私のブログで説明しているように、 Task.Run
は、実装ではなく呼び出しに使用する必要があります 。
実例
(これは完全に異なるシナリオです)
タスクGetUserByNameAsync(string userName)のどの実装が正しいでしょうか?
どちらでもかまいません。 async
とawait
のあるものには余分なオーバーヘッドがありますが、実行時に目立たないでしょう(await
ingが実際にI/Oを実行すると仮定すると、一般的な場合に当てはまります)。
メソッドに他のコードがある場合は、async
とawait
のコードの方が適していることに注意してください。これはよくある間違いです。
Task<string> MyFuncAsync()
{
using (var client = new HttpClient())
return client.GetStringAsync("http://www.example.com/");
}
この場合、HttpClient
はタスクが完了する前に破棄されます。
注意すべきもう1つの点は、タスクを返す例外beforeのスロー方法が異なることです。
Task<string> MyFuncAsync(int id)
{
... // Something that throws InvalidOperationException
return OtherFuncAsync();
}
async
がないため、例外はnot返されたタスクに配置されます。直接投げられます。これは、タスクをawait
するだけではなく、より複雑なことを行う場合、呼び出し元のコードを混乱させる可能性があります。
var task1 = MyFuncAsync(1); // Exception is thrown here.
var task2 = MyFuncAsync(2);
...
try
{
await Task.WhenAll(task1, task2);
}
catch (InvalidOperationException)
{
// Exception is not caught here. It was thrown at the first line.
}