C# でのタスク非同期プログラミング モデルThe Task asynchronous programming model in C#

タスク非同期プログラミング モデル (TAP) では、非同期コードに対する抽象化が提供されます。The Task asynchronous programming model (TAP) provides an abstraction over asynchronous code. コードは、通常と同じようにステートメントのシーケンスとして記述します。You write code as a sequence of statements, just like always. 次のステートメントが始まる前に、各ステートメントが完了するものとして、コードを読むことができます。You can read that code as though each statement completes before the next begins. これらのステートメントの一部は処理を開始し、進行中の作業を表す Task を返す可能性があるので、コンパイラではいくつかの変換が実行されます。The compiler performs a number of transformations because some of those statements may start work and return a Task that represents the ongoing work.

それがこの構文の目的です。コードは、順番に実行されるステートメントのように書かれていますが、外部リソースの割り当てとタスク完了のタイミングに基づいて、はるかに複雑な順序で実行されます。That's the goal of this syntax: enable code that reads like a sequence of statements, but executes in a much more complicated order based on external resource allocation and when tasks complete. それは、人が非同期のタスクを含むプロセスの指示を与えるのと似ています。It's analogous to how people give instructions for processes that include asynchronous tasks. この記事では、朝食を作る手順を例として使用し、async キーワードと await キーワードによって、一連の非同期命令を含むコードがどのように理解しやすくなるのかを見ていきます。Throughout this article, you'll use an example of instructions for making a breakfast to see how the async and await keywords make it easier to reason about code that includes a series of asynchronous instructions. 朝食を作る方法について説明する次の一覧のような手順を作成します。You'd write the instructions something like the following list to explain how to make a breakfast:

  1. コーヒーをカップに注ぐ。Pour a cup of coffee.
  2. フライパンを熱し、卵を 2 個焼く。Heat up a pan, then fry two eggs.
  3. ベーコンを 3 切れ焼く。Fry three slices of bacon.
  4. パンを 2 枚焼く。Toast two pieces of bread.
  5. トーストにバターとジャムを塗る。Add butter and jam to the toast.
  6. オレンジ ジュースをグラスに注ぐ。Pour a glass of orange juice.

料理の経験があれば、これらの手順を非同期的に実行するでしょう。If you have experience with cooking, you'd execute those instructions asynchronously. 卵用のフライパンを熱し始めてから、ベーコン用を始めます。you'd start warming the pan for eggs, then start the bacon. トースターにパンを入れたら、卵を焼き始めます。You'd put the bread in the toaster, then start the eggs. プロセスの各ステップで、あるタスクを開始したら、準備ができているタスクに注意を向けます。At each step of the process, you'd start a task, then turn your attention to tasks that are ready for your attention.

朝食の準備は、並列ではない非同期作業のよい例です。Cooking breakfast is a good example of asynchronous work that isn't parallel. 1 人 (つまり 1 つのスレッド) で、これらすべてのタスクを処理できます。One person (or thread) can handle all these tasks. 朝食の例を続けると、1 人で、最初の作業が完了する前に次の作業を開始して、非同期に朝食を作ることができます。Continuing the breakfast analogy, one person can make breakfast asynchronously by starting the next task before the first completes. 調理はそれを監視している人がいるかどうかに関係なく進行します。The cooking progresses whether or not someone is watching it. 卵用のフライパンを熱し始めたらすぐに、ベーコンを焼き始めることができます。As soon as you start warming the pan for the eggs, you can begin frying the bacon. ベーコンを焼き始めたら、パンをトースターに入れることができます。Once the bacon starts, you can put the bread into the toaster.

並列アルゴリズムの場合は、複数の料理人 (つまりスレッド) が必要です。For a parallel algorithm, you'd need multiple cooks (or threads). 1 人は卵を焼き、1 人はベーコンを焼く、といった具合です。One would make the eggs, one the bacon, and so on. それぞれは、1 つのタスクだけに集中します。Each one would be focused on just that one task. 各料理人 (つまりスレッド) は、ベーコンを裏返すことができるようになるまで、またはトーストが飛び出すまで待つ間は、同期的にブロックされます。Each cook (or thread) would be blocked synchronously waiting for bacon to be ready to flip, or the toast to pop.

それでは、これと同じ命令が C# ステートメントとして書かれている場合を考えてみましょう。Now, consider those same instructions written as C# statements:

static void Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

コンピューターによって、人と同じように命令が解釈されることはありません。Computers don't interpret those instructions the same way people do. コンピューターでは、作業が完了するまで各ステートメントはブロックされ、完了すると次のステートメントに進みます。The computer will block on each statement until the work is complete before moving on to the next statement. それではおいしい朝食はできません。That creates an unsatisfying breakfast. 後のタスクは、前のタスクが完了するまで開始されません。The later tasks wouldn't be started until the earlier tasks had completed. 朝食の支度でこんなことをしていると、ずっと長い時間がかかり、提供される前に冷めてしまう料理もあるでしょう。It would take much longer to create the breakfast, and some items would have gotten cold before being served.

上のような手順をコンピューターに非同期に実行させたい場合は、非同期のコードを書く必要があります。If you want the computer to execute the above instructions asynchronously, you must write asynchronous code.

今日書いているプログラムでは、このようなことを考慮することが重要です。These concerns are important for the programs you write today. クライアント プログラムを書くときは、ユーザー入力に対する UI の応答性をよくする必要です。When you write client programs, you want the UI to be responsive to user input. Web からデータをダウンロードしている間、電話がフリーズしたようになるアプリケーションではいけません。Your application shouldn't make a phone appear frozen while it's downloading data from the web. サーバーのプログラムを書くときは、スレッドがブロックされては困ります。When you write server programs, you don't want threads blocked. それらのスレッドは、他の要求を処理しているかもしれません。Those threads could be serving other requests. 非同期の代替手段が存在する場合に同期コードを使用すると、低コストでスケールアウトする能力が低下してしまいます。Using synchronous code when asynchronous alternatives exist hurts your ability to scale out less expensively. ブロックされたスレッドに料金を支払うことになります。You pay for those blocked threads.

よくできた最新のアプリケーションには、非同期コードが必要です。Successful modern applications require asynchronous code. 言語のサポートがない場合、非同期コードを書くには、コールバック、完了イベント、またはコードの本来の意図をわかりにくくしてしまうような他の手段が必要でした。Without language support, writing asynchronous code required callbacks, completion events, or other means that obscured the original intent of the code. 同期コードの利点は、理解しやすいことです。The advantage of the synchronous code is that it's easy to understand. アクションが 1 ステップずつ行われれば、スキャンも理解も容易です。The step-by-step actions make it easy to scan and understand. 従来の非同期モデルでは、開発者はコードの基本的な処理ではなく、コードの非同期的性質に注目することを余儀なくされました。Traditional asynchronous models forced you to focus on the asynchronous nature of the code, not on the fundamental actions of the code.

ブロックするのではなく待機するDon't block, await instead

上記のコードは、非同期的な操作を実行するために同期的なコードを作成するという、不適切な手法の例です。The preceding code demonstrates a bad practice: constructing synchronous code to perform asynchronous operations. このようなコードを書くと、それを実行するスレッドは、他の作業を行うことができません。As written, this code blocks the thread executing it from doing any other work. 何らかのタスクの処理中は割り込まれません。It won't be interrupted while any of the tasks are in progress. パンを入れた後でトースターをじっと見詰めているようなものです。It would be as though you stared at the toaster after putting the bread in. トーストが飛び出すまで、誰かから話し掛けられても無視するでしょう。You'd ignore anyone talking to you until the toast popped.

タスクの実行中にスレッドをブロックしないように、このコードを更新することから始めましょう。Let's start by updating this code so that the thread doesn't block while tasks are running. await キーワードを使用すると、ブロックしない方法でタスクを開始し、タスクが完了したら実行を継続できます。The await keyword provides a non-blocking way to start a task, then continue execution when that task completes. 朝食作成コードの簡単な非同期バージョンは、次のスニペットのようになります。A simple asynchronous version of the make a breakfast code would look like the following snippet:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    Egg eggs = await FryEggs(2);
    Console.WriteLine("eggs are ready");
    Bacon bacon = await FryBacon(3);
    Console.WriteLine("bacon is ready");
    Toast toast = await ToastBread(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");
}

このコードでは、卵またはベーコンを調理している間、ブロックすることはありません。This code doesn't block while the eggs or the bacon are cooking. ただし、このコードは他のタスクを開始しません。This code won't start any other tasks though. まだ、トースターにトーストを入れた後、トーストが飛び出すまで眺めています。You'd still put the toast in the toaster and stare at it until it pops. ただし、少なくとも誰かが注意を引こうとしたら反応するようにはなります。But at least, you'd respond to anyone that wanted your attention. 複数の注文を受けるレストランでは、料理人は最初の朝食を作っている間に、別の朝食を作り始めます。In a restaurant where multiple orders are placed, the cook could start another breakfast while the first is cooking.

この場合、開始してまだ完了していないタスクを待っている間、朝食作業スレッドはブロックされません。Now, the thread working on the breakfast isn't blocked while awaiting any started task that hasn't yet finished. 一部のアプリケーションでは、必要な変更はこれですべてです。For some applications, this change is all that's needed. GUI アプリケーションは、この変更だけで引き続きユーザーに応答します。A GUI application still responds to the user with just this change. ただし、このシナリオでは、これだけでは十分ではありません。However, for this scenario, you want more. 各コンポーネントのタスクを順番に実行したくはありません。You don't want each of the component tasks to be executed sequentially. 前のタスクの完了を待機する前に、各コンポーネントのタスクを開始した方がよさそうです。It's better to start each of the component tasks before awaiting the previous task's completion.

タスクを同時に開始するStart tasks concurrently

多くのシナリオでは、複数の独立したタスクをすぐに開始する必要があります。In many scenarios, you want to start several independent tasks immediately. そうすれば、各タスクが完了したら、準備のできている他のタスクを続行できます。Then, as each task finishes, you can continue other work that's ready. 朝食でたとえるなら、もっと早く朝食を準備できます。In the breakfast analogy, that's how you get breakfast done more quickly. また、すべての作業がほぼ同じタイミングで終了します。You also get everything done close to the same time. できたての朝食を食べられます。You'll get a hot breakfast.

System.Threading.Tasks.Task および関連する型を使用して、進行中のタスクについて判断できます。The System.Threading.Tasks.Task and related types are classes you can use to reason about tasks that are in progress. それを使用すると、実際の朝食作成方法にさらに近いコードを記述できます。That enables you to write code that more closely resembles the way you'd actually create breakfast. 卵、ベーコン、トーストの調理を同時に始めます。You'd start cooking the eggs, bacon, and toast at the same time. それぞれでアクションが必要になったら、そのタスクに注意を向け、次のアクションを行ってから、注意が必要な他のタスクを待ちます。As each requires action, you'd turn your attention to that task, take care of the next action, then await for something else that requires your attention.

タスクを開始し、作業を表す Task オブジェクトを保持します。You start a task and hold on to the Task object that represents the work. 各タスクを await (待機) してから、結果を処理します。You'll await each task before working with its result.

朝食コードに対してこれらの変更を行いましょう。Let's make these changes to the breakfast code. 最初のステップは、操作のタスクを開始するときに、それらを待機するのではなく、保存することです。The first step is to store the tasks for operations when they start, rather than awaiting them:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Task<Bacon> baconTask = FryBacon(3);
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Console.WriteLine("Breakfast is ready!");

次に、ベーコンと卵の await ステートメントを、メソッドの最後の朝食提供前に移動します。Next, you can move the await statements for the bacon and eggs to the end of the method, before serving breakfast:

Coffee cup = PourCoffee();
Console.WriteLine("coffee is ready");
Task<Egg> eggTask = FryEggs(2);
Task<Bacon> baconTask = FryBacon(3);
Task<Toast> toastTask = ToastBread(2);
Toast toast = await toastTask;
ApplyButter(toast);
ApplyJam(toast);
Console.WriteLine("toast is ready");
Juice oj = PourOJ();
Console.WriteLine("oj is ready");

Egg eggs = await eggTask;
Console.WriteLine("eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("bacon is ready");

Console.WriteLine("Breakfast is ready!");

上のコードの方がより適切に動作します。The preceding code works better. すべての非同期タスクを一度に開始します。You start all the asynchronous tasks at once. 結果が必要なときにのみ、各タスクを待機します。You await each task only when you need the results. 上記のコードは、異なるマイクロサービスに要求を行って 1 つのページに結果をまとめる Web アプリケーションのコードに似ているかもしれません。The preceding code may be similar to code in a web application that makes requests of different microservices, then combines the results into a single page. すべての要求をすぐに行った後、すべてのタスクを await して、Web ページを作成します。You'll make all the requests immediately, then await all those tasks and compose the web page.

タスクの合成Composition with tasks

トーストを除き、朝食のすべての準備が同時に整います。You have everything ready for breakfast at the same time except the toast. トーストの作成は、非同期操作 (パンを焼く) と同期操作 (バターとジャムを塗る) の合成です。Making the toast is the composition of an asynchronous operation (toasting the bread), and synchronous operations (adding the butter and the jam). このコードの更新では、重要な概念が示されています。Updating this code illustrates an important concept:

重要

非同期操作とその後の同期操作の合成は、非同期操作です。The composition of an asynchronous operation followed by synchronous work is an asynchronous operation. 言い方を変えれば、操作の一部が非同期である場合、操作全体が非同期になります。Stated another way, if any portion of an operation is asynchronous, the entire operation is asynchronous.

上記のコードでは、Task または Task<TResult> オブジェクトを使用して実行中のタスクを保持できることを示しました。The preceding code showed you that you can use Task or Task<TResult> objects to hold running tasks. 結果を使用する前に、各タスクを await します。You await each task before using its result. 次のステップは、他の作業の組み合わせを表すメソッドを作成することです。The next step is to create methods that represent the combination of other work. 朝食を提供するには、バターとジャムを塗る前にパンを焼くタスクを待機します。Before serving breakfast, you want to await the task that represents toasting the bread before adding butter and jam. 次のコードでその作業を表すことができます。You can represent that work with the following code:

async Task<Toast> makeToastWithButterAndJamAsync(int number)
{
    var plainToast = await ToastBreadAsync(number);
    ApplyButter(plainToast);
    ApplyJam(plainToast);
    return plainToast;
}

上のメソッドのシグニチャには async 修飾子が付いています。The preceding method has the async modifier in its signature. それにより、コンパイラに対して、このメソッドに await ステートメントが含まれることが通知されます。それには非同期操作が含まれています。That signals to the compiler that this method contains an await statement; it contains asynchronous operations. このメソッドは、パンを焼いてからバターとジャムを塗るタスクを表します。This method represents the task that toasts the bread, then adds butter and jam. このメソッドからは、これら 3 つの操作の合成を表す Task<TResult> が返されます。This method returns a Task<TResult> that represents the composition of those three operations. これで、コードのメイン ブロックは次のようになります。The main block of code now becomes:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = makeToastWithButterAndJamAsync(2);

    var eggs = await eggsTask;
    Console.WriteLine("eggs are ready");
    var bacon = await baconTask;
    Console.WriteLine("bacon is ready");
    var toast = await toastTask;
    Console.WriteLine("toast is ready");
    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");

    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> makeToastWithButterAndJamAsync(int number)
    {
        var plainToast = await ToastBreadAsync(number);
        ApplyButter(plainToast);
        ApplyJam(plainToast);
        return plainToast;
    }
}

この変更では、非同期コードを使用するための重要な手法が示されています。The previous change illustrated an important technique for working with asynchronous code. タスクを返す新しいメソッドに操作を分離することで、タスクを合成します。You compose tasks by separating the operations into a new method that returns a task. そのタスクを待機するタイミングを選択できます。You can choose when to await that task. 他のタスクを同時に開始できます。You can start other tasks concurrently.

タスクを効率的に待機するAwait tasks efficiently

上記のコードの最後にある一連の await ステートメントは、Task クラスのメソッドを使用することによって改良できます。The series of await statements at the end of the preceding code can be improved by using methods of the Task class. それらの API の 1 つは WhenAll であり、これにより次のコードで示すように、引数リストのすべてのタスクが完了すると完了する Task が返されます。One of those APIs is WhenAll, which returns a Task that completes when all the tasks in its argument list have completed, as shown in the following code:

await Task.WhenAll(eggTask, baconTask, toastTask);
Console.WriteLine("eggs are ready");
Console.WriteLine("bacon is ready");
Console.WriteLine("toast is ready");
Console.WriteLine("Breakfast is ready!");

もう 1 つのオプションは、WhenAny を使用することです。これは、引数のいずれかが完了すると完了する Task<Task> が返されます。Another option is to use WhenAny, which returns a Task<Task> that completes when any of its arguments completes. 返されたタスクを待機して、既に完了したことを把握できます。You can await the returned task, knowing that it has already finished. 次のコードでは、WhenAny を使用して最初のタスクが完了するのを待った後、その結果を処理する方法が示されています。The following code shows how you could use WhenAny to await the first task to finish and then process its result. 完了したタスクの結果を処理した後、WhenAny に渡すタスクの一覧からその完了したタスクを削除します。After processing the result from the completed task, you remove that completed task from the list of tasks passed to WhenAny.

var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
while (allTasks.Any())
{
    Task finished = await Task.WhenAny(allTasks);
    if (finished == eggsTask)
    {
        Console.WriteLine("eggs are ready");
        allTasks.Remove(eggsTask);
        var eggs = await eggsTask;
    } else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
        allTasks.Remove(baconTask);
        var bacon = await baconTask;
    } else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
        allTasks.Remove(toastTask);
        var toast = await toastTask;
    } else
            allTasks.Remove(finished);
}
Console.WriteLine("Breakfast is ready!");

すべてを変更した後、最終バージョンの Main は次のコードのようになります。After all those changes, the final version of Main looks like the following code:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");
    var eggsTask = FryEggsAsync(2);
    var baconTask = FryBaconAsync(3);
    var toastTask = makeToastWithButterAndJamAsync(2);

    var allTasks = new List<Task>{eggsTask, baconTask, toastTask};
    while (allTasks.Any())
    {
        Task finished = await Task.WhenAny(allTasks);
        if (finished == eggsTask)
        {
            Console.WriteLine("eggs are ready");
            allTasks.Remove(eggsTask);
            var eggs = await eggsTask;
        } else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
            allTasks.Remove(baconTask);
            var bacon = await baconTask;
        } else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
            allTasks.Remove(toastTask);
            var toast = await toastTask;
        } else
                allTasks.Remove(finished);
    }
    Console.WriteLine("Breakfast is ready!");

    async Task<Toast> makeToastWithButterAndJamAsync(int number)
    {
        var plainToast = await ToastBreadAsync(number);
        ApplyButter(plainToast);
        ApplyJam(plainToast);
        return plainToast;
    }
}

この最後のコードは非同期です。This final code is asynchronous. 人が朝食を作る方法が、より正確に反映されています。It more accurately reflects how a person would cook a breakfast. 上のコードを、この記事の最初のコード サンプルと比較してください。Compare the preceding code with the first code sample in this article. 中核となるアクションはコードを読むと明らかです。The core actions are still clear from reading the code. このコードは、この記事の最初にある朝食の作成手順と同じように読むことができます。You can read this code the same way you'd read those instructions for making a breakfast at the beginning of this article. async および await の言語機能により、手順書に従うためにすべての人が行う変換が提供されます。つまり、可能になったらタスクを開始し、タスク完了の待機をブロックしないようにします。The language features for async and await provide the translation every person makes to follow those written instructions: start tasks as you can and don't block waiting for tasks to complete.