Lập trình bất đồng bộ (asynchronous programming) | Tự học ICT

Trong bài học này chúng ta sẽ học cách vận dụng kỹ thuật lập trình bất đồng bộ (asynchronous programming) trong lập trình socket giúp tăng hiệu suất của server và tăng khả năng đáp ứng (responsiveness) của client.

Trong bài học trước chúng ta đã xem xét kỹ thuật lập trình đa luồng. Kỹ thuật này giúp chúng ta xây dựng chương trình server hiệu quả hơn và có thể phục vụ cùng lúc nhiều client. Sử dụng kỹ thuật này cũng giúp xây dựng client phản ứng tốt hơn với người dùng. Kỹ thuật đa luồng giúp client không bị treo khi thực hiện các tác vụ kéo dài.

Ngoài kỹ thuật đa luồng, trong lập trình socket cũng rất thường xuyên sử dụng kỹ thuật bất đồng bộ với cùng một mục tiêu.

Khái niệm bất đồng bộ (Asynchronous Programming)

Mô hình đồng bộ (synchronous)

Trong mô hình lập trình quen thuộc của chúng ta, các công việc được thực hiện theo trật tự thời gian. Công việc sắp xếp trước thực hiện xong mới đến lượt công việc tiếp theo.

Giả sử, có ba công việc được sắp xếp theo trình tự là T1, T2, T3; Thời gian thực hiện T1 là t1 giây, với T2 là t2 giây, T3 là t3 giấy.

Mô hình đồng bộ của ba nhiệm vụ T1, T2, T3Mô hình đồng bộ của ba nhiệm vụ T1, T2, T3Mô hình đồng bộ (synchronous) của ba nhiệm vụ T1, T2, T3

Khi nhiệm vụ T1 đã được bắt đầu thực hiện thì phải chờ T1 kết thúc, T2 mới được bắt đầu (sau t1 s). Một khi T2 đã bắt đầu thì phải chờ T2 kết thúc, T3 mới được bắt đầu (sau t1 + t2 s). Tổng thời gian thực hiện của cả ba nhiệm vụ là t1 + t2 + t3 s.

Mô hình lập trình theo đó các công việc bắt đầu kết thúc theo đúng trình tự thời gian như trên được gọi là mô hình lập trình đồng bộ. Mô hình này đơn giản và phù hợp với cách suy nghĩ bình thường.

Trong mô hình đa luồng ở bài trước, mỗi luồng thực chất cũng là một chuỗi lệnh đồng bộ.

Mô hình bất đồng bộ (asynchronous)

Giả sử (vẫn 3 nhiệm vụ T1, T2, T3 như trên) bây giờ chúng ta không chờ T1 kết thúc mà bắt đầu luôn T2, ngay sau khi bắt đầu T2 chúng ta bắt đầu luôn T3.

Mô hình bất đồng bộ (asynchronous) của ba nhiệm vụ T1, T2, T3Mô hình bất đồng bộ (asynchronous) của ba nhiệm vụ T1, T2, T3Mô hình bất đồng bộ (asynchronous) của ba nhiệm vụ T1, T2, T3

Trong mô hình này, nhiệm vụ sau không phải chờ nhiệm vụ trước kết thúc nữa. Tổng thời gian thực hiện của ba nhiệm vụ không phải là t1 + t2 + t3 nữa mà chỉ nhỉnh hơn thời gian thực hiện nhiệm vụ dài nhất. Rõ ràng mô hình này có lợi thế hơn về thời gian thực hiện.

Mô hình trong đó các nhiệm vụ không phải tuân thủ theo trình tự thời gian như trên được gọi là mô hình bất đồng bộ (asynchronous). Như vậy mô hình bất đồng bộ cũng cho phép thực hiện song song nhiều nhiệm vụ cùng lúc.

Trong lập trình socket, mô hình bất đồng bộ được sử dụng rất phổ biến ở cả client và server. Đối với server, mô hình này cho phép xử lý đồng thời nhiều client và phát huy tốt khả năng xử lý song song của server. Đối với client, nó giúp chương trình không bị treo giao diện khi thực hiện các nhiệm vụ kéo dài.

Bất đồng bộ và đa luồng

Bất đồng bộ và đa luồng là hai khái niệm và mô hình khác nhau, mặc dù cùng hướng tới những mục tiêu tương tự. Mô hình bất đồng bộ có thể thực hiện trên một luồng hoặc trên nhiều luồng khác nhau, tùy từng nền tảng cụ thể.

Để dễ hình dung, hãy xem ví dụ sau.

Giả sử trong một nhà bếp có một người đầu bếp (rất chăm chỉ nhưng cũng rất máy móc :)). Bình thường, tại mỗi thời điểm anh ta chỉ nấu đúng một món. Nấu xong món này anh ta mới chuyển sang nấu món thứ hai. Đây là mô hình quen thuộc nhất của chúng ta: đồng bộ và đơn luồng.

Giả sử nhà hàng muốn tăng hiệu suất phục vụ khác, giờ có hai phương án: hoặc thuê thêm đầu bếp, hoặc bắt anh đầu bếp kia phải tối ưu hóa hoạt động của mình.

Phương án tăng thêm số đầu bếp giống như bổ sung thêm luồng phụ, tức là mô hình đa luồng. Phương án này có vấn đề. Khi có quá nhiều đầu bếp mà số dụng cụ nấu ăn không đổi sẽ dẫn đến tranh nhau dùng (cạnh tranh về tài nguyên). Thuê thêm đầu bếp cũng tốn kém cho nhà hàng.

Giờ yêu cầu anh đầu bếp như sau. Giả sử bắt đầu xào rau, vì rau cần một lúc mới chín, trong thời gian đó anh ta có thể chuyển qua xào thịt. Trong lúc xào thịt sẽ liên tục kiểm tra xem rau chín chưa. Nếu chính thì múc ra đĩa rồi quay lại xào thịt tiếp. Nếu trong lúc xào thịt mà có yêu cầu món mới thì lại tiếp tục bố trí thời gian chuyển qua lại giữa các món như vậy. Phương án này tương tự như lối suy nghĩ theo mô hình bất đồng bộ.

Kỹ thuật lập trình socket bất đồng bộ

Mô hình bất đồng bộ có nhiều ưu điểm và được xây dựng trên hầu hết các nền tảng phát triển ứng dụng. Trên .NET framework có ba mô hình lập trình lập trình bất đồng bộ: APM (Asynchronous Programming Model), EAP (Event-based Asynchronous Pattern, và TAP (Task-based Asynchronous Programming).

APM ra đời từ đầu cùng với những phiên bản .NET đầu tiên. EAP ra đời sau và hoạt động dựa trên mô hình sự kiện. APM và EAP không được khuyến khích sử dụng nữa.
TAP xuất hiện gần đây nhất và thay đổi hoàn toàn cách lập trình bất đồng bộ. TAP được xây dựng trên bộ thư viện TPL (Task Parallel Library, dùng cho xử lý song song).

Mô hình APM (Asynchronous Programming Model)

Để nắm được kỹ thuật cơ bản của APM áp dụng trong lập trình socket, chúng ta cùng thực hiện lại bài tập lập trình socket Tcp cơ bản nhưng áp dụng kỹ thuật lập trình bất đồng bộ APM trên server.

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Client
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.Title = "Tcp Client";
            Console.Write("Server IP address: ");
            var serverIpStr = Console.ReadLine();
            var serverIp = IPAddress.Parse(serverIpStr);
            var serverPort = 1308;
            var serverEndpoint = new IPEndPoint(serverIp, serverPort);
            var size = 1024;
            while (true)
            {
                Console.Write("# Text >>> ");
                var text = Console.ReadLine();
                var socket = new Socket(SocketType.Stream, ProtocolType.Tcp);
                socket.Connect(serverEndpoint);
                var sendBuffer = Encoding.ASCII.GetBytes(text);
                socket.Send(sendBuffer);
                socket.Shutdown(SocketShutdown.Send);
                var receiveBuffer = new byte[size];
                var length = socket.Receive(receiveBuffer);
                var result = Encoding.ASCII.GetString(receiveBuffer, 0, length);
                socket.Close();
                Console.WriteLine($">>> {result}");
            }
        }
    }
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Server
{
    internal class Program
    {
        private static void Main(string[] args)
        {
            Console.Title = "Tcp Server";
            var listener = new TcpListener(IPAddress.Any, 1308);
            listener.Start();
            listener.BeginAcceptSocket(AcceptCallback, listener);
            // dừng màn hình
            Console.ReadLine();
        }
        private static readonly int _size = 1024;
        private static readonly byte[] _buffer = new byte[_size];
        private static void AcceptCallback(IAsyncResult ar)
        {
            var listener = ar.AsyncState as TcpListener;
            listener.BeginAcceptSocket(AcceptCallback, listener);
            var socket = listener.EndAcceptSocket(ar);
            socket.BeginReceive(_buffer, 0, _size, SocketFlags.None, ReceiveCallback, socket);
        }
        private static void ReceiveCallback(IAsyncResult ar)
        {
            var socket = ar.AsyncState as Socket;
            int count = socket.EndReceive(ar);
            var request = Encoding.ASCII.GetString(_buffer, 0, count);
            Console.WriteLine($"Received: {request}");
            var response = request.ToUpper();
            var buffer = Encoding.ASCII.GetBytes(response);
            socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, socket);
        }
        private static void SendCallback(IAsyncResult ar)
        {
            var socket = ar.AsyncState as Socket;
            int count = socket.EndSend(ar);
            Console.WriteLine($"{count} bytes have been sent to client");
        }
    }
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Server
{
    internal class Program
    {
        private static readonly TcpListener _listener = new TcpListener(IPAddress.Any, 1308);
        private static Socket _socket;
        private static void Main(string[] args)
        {
            Console.Title = "Tcp Server";
            _listener.Start();
            _listener.BeginAcceptSocket(AcceptCallback, null);
            // dừng màn hình
            Console.ReadLine();
        }
        private static readonly int _size = 1024;
        private static readonly byte[] _buffer = new byte[_size];
        private static void AcceptCallback(IAsyncResult ar)
        {
            _listener.BeginAcceptSocket(AcceptCallback, _listener);
            _socket = _listener.EndAcceptSocket(ar);
            _socket.BeginReceive(_buffer, 0, _size, SocketFlags.None, ReceiveCallback, null);
        }
        private static void ReceiveCallback(IAsyncResult ar)
        {
            int count = _socket.EndReceive(ar);
            var request = Encoding.ASCII.GetString(_buffer, 0, count);
            Console.WriteLine($"Received: {request}");
            var response = request.ToUpper();
            var buffer = Encoding.ASCII.GetBytes(response);
            _socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, SendCallback, null);
        }
        private static void SendCallback(IAsyncResult ar)
        {
            int count = _socket.EndSend(ar);
            Console.WriteLine($"{count} bytes have been sent to client");
        }
    }
}
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
namespace Server
{
    internal class Program
    {
        private static readonly TcpListener _listener = new TcpListener(IPAddress.Any, 1308);
        private static Socket _socket;
        private static void Main(string[] args)
        {
            Console.Title = "Tcp Server";
            _listener.Start();
            var ar = _listener.BeginAcceptSocket(null, null);
            AcceptCallback(ar);
            // dừng màn hình
            Console.ReadLine();
        }
        private static readonly int _size = 1024;
        private static readonly byte[] _buffer = new byte[_size];
        private static void AcceptCallback(IAsyncResult ar)
        {
            _listener.BeginAcceptSocket(AcceptCallback, _listener);
            _socket = _listener.EndAcceptSocket(ar);
            var iar = _socket.BeginReceive(_buffer, 0, _size, SocketFlags.None, null, null);
            ReceiveCallback(iar);
        }
        private static void ReceiveCallback(IAsyncResult ar)
        {
            int count = _socket.EndReceive(ar);
            var request = Encoding.ASCII.GetString(_buffer, 0, count);
            Console.WriteLine($"Received: {request}");
            var response = request.ToUpper();
            var buffer = Encoding.ASCII.GetBytes(response);
            var iar = _socket.BeginSend(buffer, 0, buffer.Length, SocketFlags.None, null, null);
            SendCallback(iar);
        }
        private static void SendCallback(IAsyncResult ar)
        {
            int count = _socket.EndSend(ar);
            Console.WriteLine($"{count} bytes have been sent to client");
        }
    }
}

Qua ví dụ trên chúng ta có thể để ý có một số phương thức của lớp Socket (và cả lớp TcpClient, TcpListener) được tổ chức thành cặp: BeginAcceptSocket/EndAcceptSocket, BeginReceive/EndReceive, BeginSend/EndSend. Đây là các cặp phương thức bất đồng bộ tương ứng của các phương thức (đồng bộ) AcceptSocket, Receive, Send mà chúng ta đã biết.

Các cặp phương thức bắt đầu bằng Begin/End như vậy là một đặc điểm nhận dạng của mô hình APM.

Nguyên lý chung của mô hình này nằm ở chỗ:

  • Phương thức Begin sẽ bắt đầu quá trình bất đồng bộ;
  • Khi quá trình bất đồng bộ kết thúc sẽ tự động gọi một phương thức được gọi chung là callback;
  • Trong phương thức callback sẽ gọi tới phương thức End để lấy kết quả từ quá trình bất đồng bộ;
  • Do quá trình gọi lẫn nhau của các phương thức như vậy, vòng lặp không thể sử dụng mà phải dùng đến đệ quy để tạo vòng lặp.

Sơ đồ quá trình bất đồng bộ ở serverSơ đồ quá trình bất đồng bộ ở serverSơ đồ quá trình bất đồng bộ ở server

Kết luận

Trong bài học này chúng ta đã cùng xem xét khái niệm bất đồng bộ và một kỹ thuật lập trình bất đồng bộ trong .NET.

Bất đồng bộ cùng với đa luồng là hai kỹ thuật sử dụng phổ biến với cả client và server giúp ứng dụng mạng hoạt động hiệu quả hơn.