Nguyên tắc không đồng bộ (Asynchronous Principle )

Hãy tưởng tượng chúng ta chuẩn bị một bữa ăn sáng gồm:

  1. Pha 1 tách cà phê
  2. Rán ốp la 2 quả trứng
  3. Nướng bánh mì

Thực hiện một cách tuần tự hay theo nguyên tắc đồng bộ (Synchronous Principle) chúng ta sẽ pha tách cà phê (pha phin) và chờ quá trình này hoàn tất. Sau đó, chúng ta tiếp tục rán ốp la 2 quả trứng và chờ hoàn thành việc rán ốp la. Cuối cùng là chúng ta sẽ nướng bánh mì. Khi nướng bánh mì xong tức là bữa sáng đã sẵn sàng. Bữa sáng trong tuần thường thời gian hạn chế và nếu áp dụng cách làm tuần tự như trên sẽ tiêu tốn rất nhiều thời gian và các món như ốp la sẽ bị nguội trong thời gian chờ bánh mì sẽ làm bữa sáng mất hương vị.

Trong thực tế, bao gồm việc chuẩn bị một bữa sáng đơn giản như trên, chúng ta không thực hiện công việc một cách tuần tự như vậy. Có thể trong thời gian chờ cà phê chúng ta sẽ tranh thủ hơ nóng chảo và trong thời gian đang hơ chảo chúng ta có thể nướng bánh mì. Trong thời gian bánh mì đang được nướng, chúng ta có thể đập hai quả trứng và cho vào chảo đang nóng và quay sang kiểm tra bánh mì đang nướng… Các công việc đan xen nhau thay vì tuần tự như trên dựa trên nguyên tắc không tuần tự hay không đồng bộ (Asynchronous Principle).

Chúng ta có thể thực hiện các công việc hằng ngày theo cách không đồng bộ nhưng máy tính thì lại khác. Các lệnh trong một chương trình máy tính được thực hiện một cách tuần tự nên sẽ tốn rất nhiều thời gian. Nếu muốn máy tính thực hiện công việc theo cách không đồng bộ, chúng ta phải viết mã không đồng bộ (asynchronous code).

Mô hình lập trình không đồng bộ

C# cung cấp mô hình lập trình không đồng bộ (The Task Asynchronous Programming Model – TAP) giúp việc triển khai mã không đồng bộ một cách hiệu quả. Các nhân tố chính trong mô hình TAP là lớp Task và các toán tử asyncawait.

Có rất nhiều thông tin về Task, asyncawait có thể tìm thấy dễ dàng trên Internet. Bài viết này không tập trung giải thích các khái niệm này mà sẽ minh họa qua một chương trình Console đơn giản. Chương trình của chúng ta sẽ thực hiện các bước để chuẩn bị một bữa ăn sáng như đã nêu ví dụ ở trên.

Chương trình chuẩn bị bữa ăn sáng

Mở Visual Studio phiên bản từ 2017 trở lên, tạo một ứng dụng Console App (.NET Core) tên BuaAnSang và lưu ứng dụng tại một vị trí phù hợp. Tập tin Program.cs có nội dung mặc định như sau:


using System;

namespace BuaAnSang

{

  class Program

  {

    static void Main(string[] args)

    {

      Console.WriteLine("Hello World!");

    }

  }

}

Trong namespace BuaAnSang thêm 3 lớp Trung, BanhmiCaphe như sau:


using System;

namespace BuaAnSang

{

  class Program

  {

    static void Main(string[] args)

    {

      Console.WriteLine("Hello World!");

    }

  }

  internal class Banhmi
  {

  }

  internal class Caphe
  {

  }

  internal class Trung
  {

  }

}

Bên trong lớp Program thêm các phương thức Pha_Caphe(), Ran_Trung_Opla()Nuong_Banhmi() như sau:


using System;

namespace BuaAnSang

{

  class Program

  {

    static void Main(string[] args)
    {

      Console.WriteLine("Hello World!");

    }

    private static Caphe Pha_Caphe() {

       Console.WriteLine("Dang pha ca phe...");

       return new Caphe();

    }

    private static Trung Ran_Trung_Opla(int So_trung) {

       Console.WriteLine("Ho nong chao...");

       Task.Delay(3000).Wait();

       Console.WriteLine($"Đap {So_trung} qua trung");

       Console.WriteLine("Ran trung...");

       Task.Delay(3000).Wait();

       Console.WriteLine("Dat trung ra dia...");

       return new Trung();

     }

    private static Banhmi Nuong_banhmi()
    {

      Console.WriteLine("Dang nuong banh mi...");

      Task.Delay(3000).Wait();

      return new Banhmi();

    }

  }

  internal class Banhmi
  {

  }

  internal class Caphe
  {

  }
  internal class Trung
  {

  }

}

Phương thức Delay(3000).Wait() của lớp Task dùng để trì hoãn 3 giây trước khi thực hiện lệnh kế tiếp. Trong hàm Main chúng ta thay đổi như sau:


static void Main(string[] args)

{

  Caphe ly = Pha_Caphe();

  Console.WriteLine("Ca phe san sang");

  Trung dia = Ran_Trung_Opla(2);

  Console.WriteLine("Trung op la san sang");

  Banhmi bm = Nuong_banhmi();

  Console.WriteLine("Banh mi san sang");

  Console.WriteLine("BUA SANG HOAN TAT. XIN MOI!!!");

  Console.Write("\nNhan phim bat ky de thoat...");

  Console.ReadKey(true);

}

Thực thi chương trình bữa ăn sáng chúng ta sẽ có kết quả như sau:

Quan sát kết quả chúng ta thấy chương trình thực hiện một cách tuần tự hay đồng bộ các bước để chuẩn bị một bữa ăn sáng.

Chương trình không đồng bộ – Version 1

Chương trình BuaAnSang thực hiện các lệnh đồng bộ và muốn thay đổi sang thực hiện không đồng bộ chúng ta cần sử dụng mô hình TAP với lớp Task và các toán tử asyncawait. Một phiên bản mới của hàm Main sẽ như sau:


static async Task Main(string[] args)

{

  Caphe ly = Pha_Caphe();

  Console.WriteLine("Ca phe san sang");

  Trung dia = await Ran_Trung_Opla(2);

  Console.WriteLine("Trung op la san sang");

  Banhmi bm = await Nuong_banhmi();

  Console.WriteLine("Banh mi san sang");

  Console.WriteLine("BUA SANG HOAN TAT. XIN MOI!!!");

  Console.Write("\nNhan phim bat ky de thoat...");

  Console.ReadKey(true);

}

Chúng ta muốn rằng khi phương thức Pha_Caphe() đang thực hiện thì các phương thức Ran_Trung_Opla()Nuong_Banhmi() cũng có thể được thực hiện mà không bị khóa (block) bởi CPU máy tính. Muốn vậy, chúng ta phải thực hiện các thay đổi sau:

– Biến phương thức Main thành phương thức thực thi không đồng bộ bằng cách:


static async Task Main(string[] args)

Hàm Main không còn trả về void như mặc định nữa mà trả về giá trị kiểu Task. Từ khóa async xác nhận phương thức Main thực hiện theo nguyên tắc không đồng bộ.

– Khi Main dùng từ khóa async, các lệnh bên trong có thể thực thi theo nguyên tắc không đồng bộ bằng cách dùng từ khóa await trước các lệnh đó, cụ thể là hai phương thức Ran_Trung_Opla() Nuong_Banhmi():


Trung dia = await Ran_Trung_Opla(2);

Banhmi bm = await Nuong_banhmi();

– Vì các phương thức Ran_Trung_Opla()Nuong_Banhmi() thực hiện theo nguyên tắc không đồng bộ nên, giống Main, chúng phải được định nghĩa là những phương thức thi không đồng bộ:


private static async Task<Trung> Ran_Trung_Opla(int So_trung) {

  ...

  await Task.Delay(3000);

  ...

  await Task.Delay(3000);

  ...

  return new Trung();

}

private static async Task<Banhmi> Nuong_banhmi()

{

  ...

  await Task.Delay(3000);

  return new Banhmi();

}

Như vậy chúng ta đã có một phiên bản đơn giản của chương trình thực thi không đồng bộ. Thực thi lệnh. Vì chúng ta thực hiện ứng dụng trong Visual Studio 2017 nên có thể xảy ra lỗi như sau:


CS5001    Program does not contain a static 'Main' method suitable for an entry point

Các phiên bản C# từ 7.0 trở về trước không cho phép đánh dấu phương thức Main như là phương thức không đồng bộ. Điều này chỉ áp dụng kể từ phiên bản C# 7.1 trờ về sau.

Để khắc phục lỗi, chúng ta nhấn chuột phải vào dự án BuaAnSang trong cửa sổ Solution Explorer, chọn Properties. Tại mục Build tìm đến nút Advanced:

Nhấn nút Advance. Trong Advanced Build Settings tại Language version chọn phiên biển C# từ 7.1 trở lên và nhấn OK.

Đóng khung Properties và thực thi lại ứng dụng. Kết quả thực thi

Với phiên bản đơn giản của chúng ta thì các nhiệm vụ vẫn được thực hiện tuần tự mặc dù các nhiệm vụ như rán trứng hay nướng bánh mì không bị block khi nhiệm vụ pha cà phê đang diễn ra.

Chương trình không đồng bộ với các nhiệm vụ hoạt động song song – Version 2

Trong Version 1, chúng ta đã tạo một phiên bản chương trình không đồng bộ nhưng các nhiệm vụ vẫn thực thi tuần tự. Trong phiên bản này (Version 2), chúng ta sẽ cải tiến lại đoạn mã trong phương thức Main cho phép các nhiệm vụ thực hiện một cách song song như sau:


static async Task Main(string[] args)

{

  Caphe ly = Pha_Caphe();

  Console.WriteLine("Ca phe san sang");

  Task<Trung> rantrung = Ran_Trung_Opla(2);

  Task<Banhmi> nuongbmi = Nuong_banhmi();

  Trung dia = await rantrung;

  Console.WriteLine("Trung op la san sang");

  Banhmi bmi = await nuongbmi;

  Console.WriteLine("Banh mi san sang");

  Console.WriteLine("BUA SANG HOAN TAT. XIN MOI!!!");

  Console.Write("\nNhan phim bat ky de thoat...");

  Console.ReadKey(true);

}

Điều thú vị ở đây là chúng ta bắt đầu tất cả các công việc cùng một lúc và lưu giữ kết quả trả về từ các phương thức Ran_Trung_Opla() và Nuong_banhmi() trong các đối tượng Task (rantrungnuongbmi). Chúng ta muốn xử lý kết quả từ công việc nào thì sẽ dùng toán tử await trước đối tượng Task tương ứng. Thực thi lại ứng dụng, kết quả sẽ khác biệt:

Chúng ta thấy rằng, khi đang hơ nóng chảo (công việc rán trứng) thì có thể tiến hành nướng bánh mì (công việc nướng bánh mì). Kết thúc thì trứng và bánh mì cùng sẵn sàng cho một bữa sáng ngon miệng.

Chương trình không đồng bộ với các nhiệm vụ hoạt động song song – Version 3 (Cuối cùng)

Phiên bản 2 (Version 2) là bước cải tiến hiệu quả để chương trình chúng ta có thể thực hiện các nhiệm vụ song song. Tuy nhiên, chúng ta vẫn có thể cải tiến một ít cho phương thức Main như sau:


static async Task Main(string[] args)

{

 Caphe ly = Pha_Caphe();

 Console.WriteLine("Ca phe san sang");

 Task<Trung> rantrung = Ran_Trung_Opla(2);

 Task<Banhmi> nuongbmi = Nuong_banhmi();

 await Task.WhenAll(rantrung,nuongbmi);

 Console.WriteLine("Trung op la san sang");

 Console.WriteLine("Banh mi san sang");

 Console.WriteLine("BUA SANG HOAN TAT. XIN MOI!!!");

 Console.Write("\nNhan phim bat ky de thoat...");

 Console.ReadKey(true);

}

Ở đây, thay vì dùng nhiều lệnh await:


Trung dia = await rantrung;

Banhmi bmi = await nuongbmi;

Chúng ta có thể dùng trong một lệnh với phương thức WhenAll của lớp Task:


await Task.WhenAll(rantrung,nuongbmi);

Tuy nhiên, khi chúng ta await tất cả các công việc thì chúng ta vẫn không biết công việc nào hoàn thành trước, công việc nào hoàn thành sau vì các công việc có thời gian thực hiện là không giống nhau. Một cách giải quyết vấn đề này là dùng phương thức WhenAny của lớp Task và phương thức Main của chúng ta cần cải tiến lại như sau:


static async Task Main(string[] args)

{

 Caphe ly = Pha_Caphe();

 Console.WriteLine("Ca phe san sang");

 var rantrung = Ran_Trung_Opla(2);

 var nuongbmi = Nuong_banhmi();

 var dscongviec = new List<Task> { rantrung, nuongbmi};

 while (dscongviec.Any()) {

   Task hoanthanh = await Task.WhenAny(dscongviec);

   if(hoanthanh == rantrung)

       Console.WriteLine("Trung op la san sang");

   else if (hoanthanh == nuongbmi)

       Console.WriteLine("Banh mi san sang");

   dscongviec.Remove(hoanthanh);

 }

 Console.WriteLine("BUA SANG HOAN TAT. XIN MOI!!!");

 Console.Write("\nNhan phim bat ky de thoat...");

 Console.ReadKey(true);

}

Chúng ta sẽ đặt tất cả công việc (rangtrung, nuongbmi) trong một danh sách công việc kiểu List<Task> và sẽ await các công việc trong danh sách này bằng phương thức WhenAny của lớp Task và đánh dấu bằng biến một biến (hoanthanh). Với cách này, chúng ta có thể dễ dàng kiểm tra công việc nào hoàn thành trước, công việc nào hoàn thành sau. Khi tất cả các công việc hoàn thành, xóa biến đánh dấu bằng phương thức Remove.

Trước khi thực thi lại chương trình và cũng là phiên bản cuối cùng, chúng ta cần xem lại các namespace chúng ta đã sử dụng để đảm bảo chương trình thực thi:


using System;

using System.Collections.Generic;

using System.Linq;

using System.Threading.Tasks;

Thực thi ứng dụng và kết quả:

Ở đây công việc nướng bánh mì diễn ra nhanh hơn rán ốp la nên sẽ hoàn thành trước.

Lời cuối

Trong bài viết này chúng ta đã tìm hiểu một cách cơ bản mô hình lập trình không đồng bộ của ngôn ngữ C#. Một lưu ý rằng, lớp Task và các toán tử awaitasync luôn đi cùng nhau. Các công việc có thể được tổ chức và await một cách hiệu quả với các phương thức WhenAll hay WhenAny từ lớp Task.