Async\Await–wydajność, część III (grupuj operacje)

Zacznijmy od synchronicznego kodu:

internal class Program
{
   private static void Main(string[] args)
   {
       ShortMethod1();
       ShortMethod2();
       ShortMethod3();
   }

   private static void ShortMethod1()
   {
       Console.WriteLine("1");
   }

   private static void ShortMethod2()
   {
       Console.WriteLine("2");
   }

   private static void ShortMethod3()
   {
       Console.WriteLine("3");
   }
}

Mamy powyżej przykład 3 metod, które wykonują bardzo proste operacje. Nie są one zbyt skomplikowane i z pewnością mogłyby być scalone do jednej metody. Z punktu widzenia wydajnościowego nie ma to jednak większego znaczenia. Prawdopodobnie kompilator zoptymalizuje to i wywoła metody w sposób inline, co tym bardziej nie będzie miało znaczenia na wydajność.

Podsumowując, dla metod  synchronicznych nie ma znaczenia czy grupujemy operacje w jedną metodę czy mamy interfejs dostarczający atomowe metody.

Przyjrzyjmy się teraz metodom asynchronicznym:

internal class Program
{
   private static void Main(string[] args)
   {
       ShortMethod1Async();
       ShortMethod2Async();
       ShortMethod3Async();
   }

   private static async void ShortMethod1Async()
   {
       Console.WriteLine("1");
   }

   private static async void ShortMethod2Async()
   {
       Console.WriteLine("2");
   }

   private static async void ShortMethod3Async()
   {
       Console.WriteLine("3");
   }
}

Proszę zauważyć, że dodaliśmy tylko jedno słowo kluczowe. Po przeczytaniu poprzedniego wpisu powinno być jasne, dlaczego powyższy kod nie jest dobrym rozwiązaniem. Dla każdej metody zostanie wygenerowana maszyna stanów:

internal class Program
{
    // Methods
    public Program();
    private static void Main(string[] args);
    [AsyncStateMachine(typeof(<ShortMethod1Async>d__0)), DebuggerStepThrough]
    private static void ShortMethod1Async();
    [AsyncStateMachine(typeof(<ShortMethod2Async>d__2)), DebuggerStepThrough]
    private static void ShortMethod2Async();
    [DebuggerStepThrough, AsyncStateMachine(typeof(<ShortMethod3Async>d__4))]
    private static void ShortMethod3Async();

    // Nested Types
    [CompilerGenerated]
    private struct <ShortMethod1Async>d__0 : IAsyncStateMachine
    {
        // Fields
        public int <>1__state;
        public AsyncVoidMethodBuilder <>t__builder;

        // Methods
        private void MoveNext();
        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine param0);
    }

    [CompilerGenerated]
    private struct <ShortMethod2Async>d__2 : IAsyncStateMachine
    {
        // Fields
        public int <>1__state;
        public AsyncVoidMethodBuilder <>t__builder;

        // Methods
        private void MoveNext();
        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine param0);
    }

    [CompilerGenerated]
    private struct <ShortMethod3Async>d__4 : IAsyncStateMachine
    {
        // Fields
        public int <>1__state;
        public AsyncVoidMethodBuilder <>t__builder;

        // Methods
        private void MoveNext();
        [DebuggerHidden]
        private void SetStateMachine(IAsyncStateMachine param0);
    }
} 

Ciało ShortMethod wygląda następująco:

[AsyncStateMachine(typeof(<ShortMethod1Async>d__0)), DebuggerStepThrough]
private static unsafe void ShortMethod1Async()
{
    <ShortMethod1Async>d__0 d__;
    AsyncVoidMethodBuilder builder;
    &d__.<>t__builder = AsyncVoidMethodBuilder.Create();
    &d__.<>1__state = -1;
    builder = &d__.<>t__builder;
    &builder.Start<<ShortMethod1Async>d__0>(&d__);
    return;
}

MoveNext z kolei zawiera:

private unsafe void MoveNext()
{
    bool flag;
    Exception exception;
Label_0000:
    try
    {
        flag = 1;
        Console.WriteLine("1");
        goto Label_0025;
    }
    catch (Exception exception1)
    {
    Label_000E:
        exception = exception1;
        this.<>1__state = -2;
        &this.<>t__builder.SetException(exception);
        goto Label_0038;
    }
Label_0025:
    this.<>1__state = -2;
    &this.<>t__builder.SetResult();
Label_0038:
    return;
}

Innymi słowy, kompilator nigdy tego nie wykona inline. Z tego względu należy przemyśleć naszą strategie implementowania kodu asynchronicznego. Takie metody zdecydowanie powinny wykonywać operacje w batch’u, a nie atomowo. Dla programisty, w c# kod wygląda bardzo prosto (tylko jedno słowo kluczowe), ale w rzeczywistości wykonujemy BARDZO wiele operacji.

Podsumowując: synchroniczne metody mogą zawierać atomowe operacje, z kolei asynchroniczne powinny wykonywać zadania grupowo.