C# - C Sharp: Tổng quan

Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực

Trong bài viết này

  1. Đừng chặn, thay vào đó hãy chờ đợi
  2. Bắt đầu nhiệm vụ đồng thời
  3. Thành phần với nhiệm vụ
  4. Ngoại lệ không đồng bộ
  5. Chờ đợi nhiệm vụ hiệu quả
  6. Bước tiếp theo

Mô hình lập trình không đồng bộ tác vụ (Task Asynchronous Programming - TAP) cung cấp sự trừu tượng hóa đối với mã không đồng bộ. Bạn viết mã dưới dạng một chuỗi các câu lệnh, giống như mọi khi. Bạn có thể đọc mã đó như thể mỗi câu lệnh hoàn thành trước khi câu lệnh tiếp theo bắt đầu. Trình biên dịch thực hiện nhiều phép biến đổi vì một số câu lệnh đó có thể bắt đầu công việc và trả về một Tác vụ đại diện cho công việc đang diễn ra.

Đó là mục tiêu của cú pháp này: cho phép mã đọc giống như một chuỗi câu lệnh nhưng thực thi theo thứ tự phức tạp hơn nhiều dựa trên sự phân bổ tài nguyên bên ngoài và khi nhiệm vụ hoàn thành. Nó tương tự như cách mọi người đưa ra hướng dẫn cho các quy trình bao gồm các tác vụ không đồng bộ. Trong suốt bài viết này, bạn sẽ sử dụng ví dụ về hướng dẫn làm bữa sáng để xem cách từ khóa async và await giúp bạn dễ dàng suy luận hơn về mã bao gồm một loạt hướng dẫn không đồng bộ. Bạn sẽ viết hướng dẫn giống như danh sách sau đây để giải thích cách làm bữa sáng:

  1. Rót một tách cà phê.
  2. Làm nóng chảo, sau đó chiên hai quả trứng.
  3. Chiên ba lát thịt xông khói.
  4. Nướng hai miếng bánh mì.
  5. Thêm bơ và mứt vào bánh mì nướng.
  6. Rót một ly nước cam.

Nếu bạn có kinh nghiệm nấu ăn, bạn sẽ thực hiện các hướng dẫn đó một cách không đồng bộ. Bạn sẽ bắt đầu làm nóng chảo chiên trứng, sau đó bắt đầu làm thịt xông khói. Bạn sẽ cho bánh mì vào máy nướng bánh mì, sau đó cho trứng vào. Ở mỗi bước của quy trình, bạn sẽ bắt đầu một nhiệm vụ, sau đó chuyển sự chú ý sang những nhiệm vụ đã sẵn sàng để bạn chú ý.

Nấu bữa sáng là một ví dụ điển hình về công việc không đồng bộ và không diễn ra song song. Một người (hoặc chủ đề) có thể xử lý tất cả các nhiệm vụ này. Tiếp tục ví dụ về bữa sáng, một người có thể làm bữa sáng không đồng bộ bằng cách bắt đầu nhiệm vụ tiếp theo trước khi nhiệm vụ đầu tiên hoàn thành. Quá trình nấu ăn vẫn tiếp tục dù có ai theo dõi hay không. Ngay khi bạn bắt đầu làm nóng chảo đựng trứng, bạn có thể bắt đầu chiên thịt xông khói. Khi thịt xông khói bắt đầu hoạt động, bạn có thể cho bánh mì vào máy nướng bánh mì.

Đối với thuật toán song song, bạn cần nhiều đầu bếp (hoặc luồng). Một người sẽ làm trứng, một người làm thịt xông khói, v.v. Mỗi người sẽ chỉ tập trung vào một nhiệm vụ đó. Mỗi đầu bếp sẽ bị chặn đồng bộ để chờ thịt xông khói sẵn sàng lật hoặc bánh mì nướng sẽ bị nổ tung.

Bây giờ, hãy xem xét các hướng dẫn tương tự được viết dưới dạng câu lệnh C#:

using System;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // Các lớp này được cố ý để trống. Ở đây những lớp này đóng vai trò đánh dấu nhằm mục đích trình diễn, không chứa property.
    internal class Bacon { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        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!");
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static Toast ToastBread(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static Bacon FryBacon(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            Task.Delay(3000).Wait();
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        private static Egg FryEggs(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            Task.Delay(3000).Wait();
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            Task.Delay(3000).Wait();
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

ảnh mô phỏng bữa sáng đồng bộ
Bữa sáng được chuẩn bị đồng bộ mất khoảng 30 phút là tổng thời gian của từng nhiệm vụ.

Máy tính không diễn giải những hướng dẫn đó giống như cách con người làm. Máy tính sẽ chặn từng câu lệnh cho đến khi công việc hoàn thành trước khi chuyển sang câu lệnh tiếp theo. Điều đó tạo nên một bữa sáng không ngon miệng. Các nhiệm vụ sau sẽ không được bắt đầu cho đến khi các nhiệm vụ trước đó được hoàn thành. Sẽ mất nhiều thời gian hơn để chuẩn bị bữa sáng và một số món sẽ bị nguội trước khi được phục vụ.

Nếu muốn máy tính thực thi các hướng dẫn trên một cách không đồng bộ, bạn phải viết mã không đồng bộ.

Những mối quan tâm này rất quan trọng đối với các chương trình bạn viết ngày hôm nay. Khi bạn viết chương trình máy khách, bạn muốn giao diện người dùng phản hồi nhanh với đầu vào của người dùng. Ứng dụng của bạn không được làm cho điện thoại có vẻ bị treo khi đang tải xuống dữ liệu từ web. Khi bạn viết chương trình máy chủ, bạn không muốn các chủ đề bị chặn. Những chủ đề đó có thể đang phục vụ các yêu cầu khác. Việc sử dụng mã đồng bộ khi tồn tại các lựa chọn thay thế không đồng bộ sẽ ảnh hưởng đến khả năng mở rộng quy mô của bạn với chi phí ít hơn. Bạn trả tiền cho những chủ đề bị chặn.

Các ứng dụng hiện đại thành công yêu cầu mã không đồng bộ. Nếu không có hỗ trợ ngôn ngữ, việc viết mã không đồng bộ sẽ yêu cầu gọi lại, sự kiện hoàn thành hoặc các phương tiện khác làm che khuất mục đích ban đầu của mã. Ưu điểm của mã đồng bộ là các hành động từng bước của nó giúp bạn dễ dàng quét và hiểu. Các mô hình không đồng bộ truyền thống buộc bạn phải tập trung vào bản chất không đồng bộ của mã chứ không phải các hành động cơ bản của mã.

Đừng chặn, thay vào đó hãy chờ đợi

Đoạn mã trên thể hiện một cách làm không tốt: xây dựng mã đồng bộ để thực hiện các hoạt động không đồng bộ. Mã này chặn luồng thực thi nó thực hiện bất kỳ công việc nào khác. Nó sẽ không bị gián đoạn trong khi bất kỳ nhiệm vụ nào đang được thực hiện. Nó sẽ giống như thể bạn nhìn chằm chằm vào máy nướng bánh mì sau khi cho bánh mì vào. Bạn sẽ phớt lờ bất kỳ ai đang nói chuyện với mình cho đến khi bánh mì nướng nổ tung.

Hãy bắt đầu bằng cách cập nhật mã này để luồng không bị chặn khi tác vụ đang chạy. Từ khóa await cung cấp một cách không bị chặn để bắt đầu một tác vụ, sau đó tiếp tục thực hiện khi tác vụ đó hoàn thành. Một phiên bản không đồng bộ đơn giản của mã tạo bữa sáng sẽ trông giống như đoạn mã sau:

static async Task Main(string[] args)
{
    Coffee cup = PourCoffee();
    Console.WriteLine("coffee is ready");

    Egg eggs = await FryEggsAsync(2);
    Console.WriteLine("eggs are ready");

    Bacon bacon = await FryBaconAsync(3);
    Console.WriteLine("bacon is ready");

    Toast toast = await ToastBreadAsync(2);
    ApplyButter(toast);
    ApplyJam(toast);
    Console.WriteLine("toast is ready");

    Juice oj = PourOJ();
    Console.WriteLine("oj is ready");
    Console.WriteLine("Breakfast is ready!");
}

Quan trọng

Tổng thời gian đã trôi qua gần giống như phiên bản đồng bộ ban đầu. Mã vẫn chưa tận dụng được một số tính năng chính của lập trình không đồng bộ.

Mẹo

Phần thân của các phương thức FryEggsAsyncFryBaconAsync, và ToastBreadAsync đều đã được cập nhật để trả về Task<Egg>Task<Bacon>, và Task<Toast> tương ứng. Các phương thức được đổi tên từ phiên bản gốc để bao gồm hậu tố "Async". Việc triển khai chúng được hiển thị như một phần của phiên bản cuối cùng ở phần sau của bài viết này.

Ghi chú

Phương thức Main trả về Task, mặc dù không có lệnh return — điều này là do thiết kế. Để biết thêm thông tin, hãy xem Đánh giá hàm async trả về void.

Mã này không chặn trong khi trứng hoặc thịt xông khói đang nấu. Tuy nhiên, mã này sẽ không bắt đầu bất kỳ tác vụ nào khác. Bạn vẫn đặt bánh mì nướng vào máy nướng bánh mì và nhìn chằm chằm vào nó cho đến khi nó nổ tung. Nhưng ít nhất, bạn sẽ phản hồi lại bất kỳ ai muốn bạn chú ý. Trong một nhà hàng có nhiều đơn hàng, người đầu bếp có thể bắt đầu một bữa sáng khác trong khi bữa sáng đầu tiên đang nấu.

Bây giờ, luồng xử lý bữa sáng không bị chặn trong khi chờ bất kỳ tác vụ đã bắt đầu nào chưa hoàn thành. Đối với một số ứng dụng, sự thay đổi này là tất cả những gì cần thiết. Ứng dụng GUI vẫn phản hồi người dùng chỉ với thay đổi này. Tuy nhiên, đối với kịch bản này, bạn muốn nhiều hơn nữa. Bạn không muốn từng tác vụ thành phần được thực thi tuần tự. Tốt hơn hết bạn nên bắt đầu từng nhiệm vụ thành phần trước khi chờ hoàn thành nhiệm vụ trước đó.

Bắt đầu nhiệm vụ đồng thời

Trong nhiều trường hợp, bạn muốn bắt đầu một số nhiệm vụ độc lập ngay lập tức. Sau đó, khi mỗi nhiệm vụ kết thúc, bạn có thể tiếp tục công việc khác đã sẵn sàng. Tương tự như bữa sáng, đó là cách bạn hoàn thành bữa sáng nhanh hơn. Bạn cũng hoàn thành mọi việc gần như cùng một lúc. Bạn sẽ có một bữa sáng nóng hổi.

System.Threading.Tasks.Task và các loại liên quan là các lớp bạn có thể sử dụng để suy luận về các nhiệm vụ đang được thực hiện. Điều đó cho phép bạn viết mã gần giống với cách bạn tạo bữa sáng hơn. Bạn sẽ bắt đầu nấu trứng, thịt xông khói và bánh mì nướng cùng một lúc. Vì mỗi việc đều yêu cầu hành động, bạn sẽ chuyển sự chú ý của mình sang nhiệm vụ đó, thực hiện hành động tiếp theo, sau đó chờ đợi việc khác đòi hỏi sự chú ý của bạn.

Bạn bắt đầu một tác vụ và giữ đối tượng Task đại diện cho công việc đó. Bạn sẽ thực hiện await từng nhiệm vụ trước khi làm việc với kết quả của nó.

Hãy thực hiện những thay đổi này đối với mã bữa sáng. Bước đầu tiên là lưu trữ các tác vụ cho các hoạt động khi chúng bắt đầu, thay vì chờ đợi chúng:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Egg eggs = await eggsTask;
Console.WriteLine("Eggs are ready");

Task<Bacon> baconTask = FryBaconAsync(3);
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

Task<Toast> toastTask = ToastBreadAsync(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!");

Đoạn mã trên sẽ không giúp bạn chuẩn bị bữa sáng nhanh hơn được nữa. Tất cả các nhiệm vụ đều được await chỉnh sửa ngay khi chúng được bắt đầu. Tiếp theo, bạn có thể di chuyển các lệnh await về thịt xông khói và trứng đến cuối phương thức, trước khi phục vụ bữa sáng:

Coffee cup = PourCoffee();
Console.WriteLine("Coffee is ready");

Task<Egg> eggsTask = FryEggsAsync(2);
Task<Bacon> baconTask = FryBaconAsync(3);
Task<Toast> toastTask = ToastBreadAsync(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 eggsTask;
Console.WriteLine("Eggs are ready");
Bacon bacon = await baconTask;
Console.WriteLine("Bacon is ready");

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

ảnh mô phỏng bữa sáng không đồng bộ
Bữa sáng được chuẩn bị không đồng bộ mất khoảng 20 phút, tiết kiệm thời gian này là do một số nhiệm vụ được thực hiện đồng thời.

Đoạn mã trên đã hoạt động tốt hơn. Bạn bắt đầu tất cả các tác vụ không đồng bộ cùng một lúc. Bạn chỉ chờ đợi mỗi nhiệm vụ khi bạn cần kết quả. Mã trên có thể tương tự như mã trong ứng dụng web đưa ra yêu cầu tới các vi dịch vụ khác nhau, sau đó kết hợp các kết quả vào một trang duy nhất. Bạn sẽ thực hiện tất cả các yêu cầu ngay lập tức, sau đó thực hiện await tất cả các nhiệm vụ đó và soạn thảo trang web.

Thành phần với nhiệm vụ

Bạn đã chuẩn bị sẵn mọi thứ cho bữa sáng cùng lúc ngoại trừ bánh mì nướng. Làm bánh mì nướng là sự kết hợp của thao tác không đồng bộ (nướng bánh mì) và thao tác đồng bộ (thêm bơ và mứt). Cập nhật mã này sẽ minh họa một khái niệm quan trọng:

Quan trọng

Thành phần của hoạt động không đồng bộ theo sau là hoạt động đồng bộ là hoạt động không đồng bộ. Nói cách khác, nếu bất kỳ phần nào của thao tác mà không đồng bộ thì toàn bộ thao tác đó sẽ là không đồng bộ.

Đoạn mã trên cho bạn thấy rằng bạn có thể sử dụng các đối tượng Task hoặc Task<TResult> để chứa các tác vụ đang chạy. Bạn thực hiện await từng nhiệm vụ trước khi sử dụng kết quả của nó. Bước tiếp theo là tạo các phương thức thể hiện sự kết hợp của các công việc khác. Trước khi phục vụ bữa sáng, bạn nên đợi nhiệm vụ nướng bánh mì trước khi thêm bơ và mứt. Bạn có thể biểu diễn công việc đó bằng đoạn mã sau:

static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
{
    var toast = await ToastBreadAsync(number);
    ApplyButter(toast);
    ApplyJam(toast);

    return toast;
}

Phương thức trên có phần sửa đổi async trong signature của nó. Điều đó báo hiệu cho trình biên dịch rằng phương thức này chứa một câu lệnh await; nó chứa các hoạt động không đồng bộ. Phương thức này thể hiện nhiệm vụ nướng bánh mì, sau đó thêm bơ và mứt. Phương thức này trả về Task<TResult> đại diện cho thành phần của ba thao tác đó. Khối mã chính bây giờ trở thành:

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!");
}

Thay đổi trên minh họa một kỹ thuật quan trọng để làm việc với mã không đồng bộ. Bạn soạn các tác vụ bằng cách tách các thao tác thành một phương thức mới trả về một tác vụ. Bạn có thể chọn thời điểm chờ đợi nhiệm vụ đó. Bạn có thể bắt đầu các nhiệm vụ khác đồng thời.

Ngoại lệ không đồng bộ

Cho đến thời điểm này, bạn đã ngầm cho rằng tất cả các nhiệm vụ này đều hoàn thành thành công. Các phương thức không đồng bộ đưa ra các ngoại lệ, giống như các phương thức đồng bộ của chúng. Hỗ trợ không đồng bộ cho các trường hợp ngoại lệ và xử lý lỗi cố gắng đạt được các mục tiêu giống như hỗ trợ không đồng bộ nói chung: Bạn nên viết mã đọc giống như một loạt các câu lệnh đồng bộ. Nhiệm vụ đưa ra ngoại lệ khi chúng không thể hoàn thành thành công. Mã máy khách có thể phát hiện những ngoại lệ đó khi một tác vụ đã bắt đầu là awaited. Ví dụ: giả sử máy nướng bánh mì bốc cháy khi đang nướng bánh mì. Bạn có thể mô phỏng điều đó bằng cách sửa đổi phương thức ToastBreadAsync để khớp với đoạn mã sau:

private static async Task<Toast> ToastBreadAsync(int slices)
{
    for (int slice = 0; slice < slices; slice++)
    {
        Console.WriteLine("Putting a slice of bread in the toaster");
    }
    Console.WriteLine("Start toasting...");
    await Task.Delay(2000);
    Console.WriteLine("Fire! Toast is ruined!");
    throw new InvalidOperationException("The toaster is on fire");
    await Task.Delay(1000);
    Console.WriteLine("Remove toast from toaster");

    return new Toast();
}

Ghi chú

Bạn sẽ nhận được cảnh báo khi biên dịch mã trên liên quan đến mã không thể truy cập được. Đó là cố ý vì một khi máy nướng bánh mì bốc cháy, hoạt động sẽ không diễn ra bình thường.

Chạy ứng dung sau khi thực hiện những thay đổi này và bạn sẽ xuất ra văn bản tương tự như sau:

Pouring coffee
Coffee is ready
Warming the egg pan...
putting 3 slices of bacon in the pan
Cooking first side of bacon...
Putting a slice of bread in the toaster
Putting a slice of bread in the toaster
Start toasting...
Fire! Toast is ruined!
Flipping a slice of bacon
Flipping a slice of bacon
Flipping a slice of bacon
Cooking the second side of bacon...
Cracking 2 eggs
Cooking the eggs ...
Put bacon on plate
Put eggs on plate
Eggs are ready
Bacon is ready
Unhandled exception. System.InvalidOperationException: The toaster is on fire
   at AsyncBreakfast.Program.ToastBreadAsync(Int32 slices) in Program.cs:line 65
   at AsyncBreakfast.Program.MakeToastWithButterAndJamAsync(Int32 number) in Program.cs:line 36
   at AsyncBreakfast.Program.Main(String[] args) in Program.cs:line 24
   at AsyncBreakfast.Program.<Main>(String[] args)

Bạn sẽ nhận thấy có khá nhiều nhiệm vụ được hoàn thành trong khoảng thời gian từ khi máy nướng bánh mì bắt lửa cho đến khi có ngoại lệ. Khi một tác vụ chạy không đồng bộ ném ra một ngoại lệ, tác vụ đó bị lỗi. Đối tượng Task giữ ngoại lệ được đưa ra trong property Task.Exception. Nhiệm vụ bị lỗi sẽ tạo ra một ngoại lệ khi chúng được chờ đợi.

Có hai cơ chế quan trọng cần hiểu: cách một ngoại lệ được lưu trữ trong một tác vụ bị lỗi và cách một ngoại lệ được giải nén và truy xuất lại khi mã đang chờ một tác vụ bị lỗi.

Khi mã chạy không đồng bộ sẽ ném ra một ngoại lệ, ngoại lệ đó sẽ được lưu trữ trong Task. Property Task.Exception là System.AggregateException vì nhiều ngoại lệ có thể được đưa ra trong quá trình làm việc không đồng bộ. Bất kỳ ngoại lệ nào được ném sẽ được thêm vào bộ collection AggregateException.InnerExceptions. Nếu property đó là null, thì một property mới là ExceptionAggregateException sẽ được tạo và ngoại lệ được ném ra là mục đầu tiên trong collection.

Tình huống phổ biến nhất đối với tác vụ bị lỗi là property Exception chứa chính xác một ngoại lệ. Khi code awaits một tác vụ bị lỗi, ngoại lệ đầu tiên trong collection AggregateException.InnerExceptions sẽ được đưa ra lại. Đó là lý do tại sao kết quả từ ví dụ này hiển thị InvalidOperationException thay vì AggregateException. Việc trích xuất ngoại lệ bên trong đầu tiên làm cho việc làm việc với các phương thức không đồng bộ giống nhất có thể với cách làm việc với các phương thức đồng bộ của chúng. Bạn có thể kiểm tra property Exception trong mã của mình khi kịch bản của bạn có thể tạo ra nhiều ngoại lệ.

Mẹo

Chúng tôi khuyên rằng mọi ngoại lệ xác thực đối số đều xuất hiện đồng bộ từ các phương thức trả về tác vụ. Để biết thêm thông tin và ví dụ về cách thực hiện, hãy xem Ngoại lệ trong các phương thức trả về tác vụ.

Trước khi tiếp tục, hãy comment hai dòng này trong phương thức ToastBreadAsync nếu bạn không muốn bắt đầu một ngọn lửa khác:

Console.WriteLine("Fire! Toast is ruined!");
throw new InvalidOperationException("The toaster is on fire");

Chờ đợi nhiệm vụ hiệu quả

Chuỗi lệnh await ở cuối đoạn mã trên có thể được cải thiện bằng cách sử dụng các phương thức của lớp Task. Một trong những API đó là WhenAll, trả về một Task hoàn thành khi tất cả các tác vụ trong danh sách đối số của nó đã hoàn thành, như minh họa trong đoạn mã sau:

await Task.WhenAll(eggsTask, baconTask, toastTask);
Console.WriteLine("Eggs are ready");
Console.WriteLine("Bacon is ready");
Console.WriteLine("Toast is ready");
Console.WriteLine("Breakfast is ready!");

Một tùy chọn khác là sử dụng WhenAny, nó trả về một kết quả hoàn thiện Task<Task> khi bất kỳ đối số nào của nó hoàn thành. Bạn có thể chờ đợi nhiệm vụ được trả về khi biết rằng nó đã hoàn thành. Đoạn mã sau cho biết cách bạn có thể sử dụng WhenAny để chờ tác vụ đầu tiên hoàn thành rồi xử lý kết quả của nó. Sau khi xử lý kết quả từ tác vụ đã hoàn thành, bạn xóa tác vụ đã hoàn thành đó khỏi danh sách các tác vụ được chuyển tới WhenAny.

var breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
while (breakfastTasks.Count > 0)
{
    Task finishedTask = await Task.WhenAny(breakfastTasks);
    if (finishedTask == eggsTask)
    {
        Console.WriteLine("Eggs are ready");
    }
    else if (finishedTask == baconTask)
    {
        Console.WriteLine("Bacon is ready");
    }
    else if (finishedTask == toastTask)
    {
        Console.WriteLine("Toast is ready");
    }
    await finishedTask;
    breakfastTasks.Remove(finishedTask);
}

Ở phần gần cuối, bạn nhìn thấy dòng await finishedTask;. Dòng await Task.WhenAny không chờ đợi nhiệm vụ đã hoàn thành. await được Task trả về bởi Task.WhenAny. Kết quả của Task.WhenAny là tác vụ đã hoàn thành (hoặc bị lỗi). Bạn nên thực hiện await lại nhiệm vụ đó, mặc dù bạn biết nó đã chạy xong. Đó là cách bạn truy xuất kết quả của nó hoặc đảm bảo rằng ngoại lệ gây ra lỗi sẽ bị loại bỏ.

Sau tất cả những thay đổi đó, phiên bản cuối cùng của mã sẽ trông như thế này:

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace AsyncBreakfast
{
    // Các lớp này được cố ý để trống. Ở đây những lớp này đóng vai trò đánh dấu nhằm mục đích trình diễn, không chứa property.
    internal class Bacon { }
    internal class Coffee { }
    internal class Egg { }
    internal class Juice { }
    internal class Toast { }

    class Program
    {
        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 breakfastTasks = new List<Task> { eggsTask, baconTask, toastTask };
            while (breakfastTasks.Count > 0)
            {
                Task finishedTask = await Task.WhenAny(breakfastTasks);
                if (finishedTask == eggsTask)
                {
                    Console.WriteLine("eggs are ready");
                }
                else if (finishedTask == baconTask)
                {
                    Console.WriteLine("bacon is ready");
                }
                else if (finishedTask == toastTask)
                {
                    Console.WriteLine("toast is ready");
                }
                await finishedTask;
                breakfastTasks.Remove(finishedTask);
            }

            Juice oj = PourOJ();
            Console.WriteLine("oj is ready");
            Console.WriteLine("Breakfast is ready!");
        }

        static async Task<Toast> MakeToastWithButterAndJamAsync(int number)
        {
            var toast = await ToastBreadAsync(number);
            ApplyButter(toast);
            ApplyJam(toast);

            return toast;
        }

        private static Juice PourOJ()
        {
            Console.WriteLine("Pouring orange juice");
            return new Juice();
        }

        private static void ApplyJam(Toast toast) =>
            Console.WriteLine("Putting jam on the toast");

        private static void ApplyButter(Toast toast) =>
            Console.WriteLine("Putting butter on the toast");

        private static async Task<Toast> ToastBreadAsync(int slices)
        {
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("Putting a slice of bread in the toaster");
            }
            Console.WriteLine("Start toasting...");
            await Task.Delay(3000);
            Console.WriteLine("Remove toast from toaster");

            return new Toast();
        }

        private static async Task<Bacon> FryBaconAsync(int slices)
        {
            Console.WriteLine($"putting {slices} slices of bacon in the pan");
            Console.WriteLine("cooking first side of bacon...");
            await Task.Delay(3000);
            for (int slice = 0; slice < slices; slice++)
            {
                Console.WriteLine("flipping a slice of bacon");
            }
            Console.WriteLine("cooking the second side of bacon...");
            await Task.Delay(3000);
            Console.WriteLine("Put bacon on plate");

            return new Bacon();
        }

        private static async Task<Egg> FryEggsAsync(int howMany)
        {
            Console.WriteLine("Warming the egg pan...");
            await Task.Delay(3000);
            Console.WriteLine($"cracking {howMany} eggs");
            Console.WriteLine("cooking the eggs ...");
            await Task.Delay(3000);
            Console.WriteLine("Put eggs on plate");

            return new Egg();
        }

        private static Coffee PourCoffee()
        {
            Console.WriteLine("Pouring coffee");
            return new Coffee();
        }
    }
}

khi có bất kỳ bữa sáng không đồng bộ nào

Phiên bản cuối cùng của bữa sáng được chuẩn bị không đồng bộ mất khoảng 6 phút vì một số tác vụ chạy đồng thời và mã giám sát nhiều tác vụ cùng một lúc và chỉ thực hiện hành động khi cần thiết.

Mã cuối cùng này không đồng bộ. Nó phản ánh chính xác hơn cách một người nấu bữa sáng. So sánh mã trước với mẫu mã đầu tiên trong bài viết này. Các hành động cốt lõi vẫn rõ ràng khi đọc mã. Bạn có thể đọc mã này giống như cách bạn đọc hướng dẫn làm bữa sáng ở đầu bài viết này. Ngôn ngữ có tính năng async và await cung cấp bản dịch mà mỗi người thực hiện để tuân theo các hướng dẫn bằng văn bản đó: bắt đầu nhiệm vụ khi bạn có thể và không chặn việc chờ hoàn thành nhiệm vụ.

Bước tiếp theo

Khám phá các kịch bản thế giới thực cho các chương trình không đồng bộ

» Tiếp: Kịch bản lập trình không đồng bộ
« Trước: Model-View-ViewModel (MVVM)
Các khóa học qua video:
Python SQL Server PHP C# Lập trình C Java HTML5-CSS3-JavaScript
Học trên YouTube <76K/tháng. Đăng ký Hội viên
Viết nhanh hơn - Học tốt hơn
Giải phóng thời gian, khai phóng năng lực
Copied !!!