web-dev-qa-db-ja.com

Task <T>を返すメソッドを実装する適切な方法

簡単にするために、重い操作を実行しているときにオブジェクトを返す必要があるメソッドがあると想像してみましょう。実装する方法は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);
}
_
18
Leri

ILからわかるように、_async/await_は、些細な非同期末尾呼び出しの場合でも、ステートマシン(および追加のTask)を作成します。

_return await Task.Run(...);
_

これにより、追加の命令と割り当てが原因でパフォーマンスが低下します。したがって、経験則は次のとおりです。メソッドが_await ..._または_return await ..._で終わり、それが唯一のawaitである場合ステートメントの場合、一般的にasyncキーワードを削除して、待機する予定のTaskを直接返すのが安全です。

これを行うことの潜在的に意図しない結果の1つは、返されたTask内で例外がスローされた場合、外部メソッドがスタックトレースに表示されないことです。

ただし、_return await ..._の場合にも隠れた落とし穴があります。待機者がConfigureAwait(false)を介してキャプチャされたコンテキストで続行するように明示的に構成されていない場合not、外側のTask(非同期状態マシンによって作成されたもの) SynchronizationContextへの最後のポストバック(awaitの直前にキャプチャされた)が終了するまで、完了状態に移行できません。これは実際の目的には役立ちませんが、何らかの理由で外部タスクをブロックすると、デッドロックが発生する可能性があります( 詳細な説明は次のとおりです そのような場合に何が起こるか)。

11

では、どちらのアプローチが正しいのでしょうか?

どちらでもない。

同期作業がある場合は、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)のどの実装が正しいでしょうか?

どちらでもかまいません。 asyncawaitのあるものには余分なオーバーヘッドがありますが、実行時に目立たないでしょう(awaitingが実際にI/Oを実行すると仮定すると、一般的な場合に当てはまります)。

メソッドに他のコードがある場合は、asyncawaitのコードの方が適していることに注意してください。これはよくある間違いです。

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.
}
12
Stephen Cleary