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. 編譯器會執行一些轉換,因為部分陳述式可能會開始工作,並傳回表示進行工作的 TaskThe 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. 在本文中,您將以準備早餐的指示為例,來了解 asyncawait 關鍵字如何讓您更輕鬆地理解包含一連串非同步指示的程式碼。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. 熱鍋,然後煎兩顆蛋。Heat up a pan, then fry two eggs.
  3. 煎三片培根。Fry three slices of bacon.
  4. 烤兩片吐司。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. 一個人 (或執行緒) 可處理所有這些工作。One person (or thread) can handle all these tasks. 繼續以早餐為例,一個人可能會以非同步方式準備早餐,不等到第一個工作完成,就開始下一個工作。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). 一個人會負責煎蛋、一個人會負責煎培根,依此類推。One would make the eggs, one the bacon, and so on. 每個人只會專注於一個工作。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. 您的應用程式不應該讓手機在從網路下載資料時呈現凍結。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. 逐步動作讓您可以輕鬆地瀏覽與了解。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. 上述程式碼可能會類似於提出不同微服務要求,然後將結果合併成單一頁面的 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 所有這些工作並撰寫網頁。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.

上述程式碼顯示您可以使用 TaskTask<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. 此方法會傳回 Task<TResult>,表示這三項作業的組合。This method returns a Task<TResult> that represents the composition of those three operations. 程式碼的 Main 區塊現在會變成: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 是 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!");

另一個選項是使用 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");
    }
    else if (finished == baconTask)
    {
        Console.WriteLine("bacon is ready");
    }
    else if (finished == toastTask)
    {
        Console.WriteLine("toast is ready");
    }
    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");
        }
        else if (finished == baconTask)
        {
            Console.WriteLine("bacon is ready");
        }
        else if (finished == toastTask)
        {
            Console.WriteLine("toast is ready");
        }
        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. asyncawait 之語言功能為所有遵循下列書面指示的人員提供轉譯:盡可能開始多個工作且不要防止等候工作完成。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.