C# - C Sharp: Lập trình song song (Concurrency programming)

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

Tiến trình và đa tiến trình

Mỗi tiến trình thường có một môi trường thực thi khép kín. Tiến trình thường có một bộ tài nguyên thời gian chạy cơ bản hoàn chỉnh, riêng tư; đặc biệt, mỗi quá trình có không gian bộ nhớ riêng. Hệ thống thường sẽ cho phép nhiều tiến trình thực thi đồng thời.

Một luồng (thread) là một đường dẫn thực thi trong một ứng dụng thực thi. Bằng cách triển khai các luồng bổ sung, ta có thể xây dựng các ứng dụng phản hồi nhanh hơn (nhưng không nhất thiết phải thực thi nhanh hơn).

Đa tiến trình

Concurrency trong hệ điều hành

Concurrency (Đồng thời) là việc thực hiện nhiều chuỗi lệnh cùng một lúc. Nó xảy ra trong hệ điều hành khi có một số luồng tiến trình chạy song song

Các luồng tiến trình đang chạy luôn giao tiếp với nhau thông qua bộ nhớ dùng chung hoặc thông báo truyền đi. Đồng thời dẫn đến việc chia sẻ tài nguyên dẫn đến các vấn đề như bế tắc và thiếu tài nguyên

Concurrency hỗ trợ trong các kỹ thuật như điều phối việc thực thi các quy trình, cấp phát bộ nhớ và lập lịch thực thi để tối đa hóa thông lượng

Ưu điểm của đồng thời

- Chạy nhiều ứng dụng: Cho phép chạy nhiều ứng dụng cùng lúc.

- Sử dụng tài nguyên tốt hơn: Nó cho phép các tài nguyên không được sử dụng bởi một ứng dụng có thể được sử dụng cho các ứng dụng khác.

Thời gian phản hồi trung bình tốt hơn: Không có đồng thời, mỗi ứng dụng phải được chạy cho đến khi hoàn thành trước khi có thể chạy ứng dụng tiếp theo.

- Hiệu suất tốt hơn: Nó cho phép hệ điều hành có hiệu suất tốt hơn. Khi một ứng dụng chỉ sử dụng bộ xử lý và một ứng dụng khác chỉ sử dụng ổ đĩa thì thời gian chạy đồng thời cả hai ứng dụng cho đến khi hoàn thành sẽ ngắn hơn so với thời gian chạy liên tiếp từng ứng dụng

Các vấn đề về Concurrency

- Không duy nhất (Non-atomic): Các hoạt động không duy nhất nhưng có thể bị gián đoạn bởi nhiều quy trình có thể gây ra sự cố.

- Điều kiện chạy đua (Race condition): Xảy ra với kết quả phụ thuộc vào tiến trình nào trong số một số tiến trình đạt đến điểm đầu tiên.

- Chặn (Blocking): Các tiến trình có thể chặn chờ tài nguyên. Một tiến trình có thể bị chặn trong thời gian dài chờ đợi đầu vào từ một thiết bị đầu cuối. Nếu quy trình được yêu cầu cập nhật định kỳ một số dữ liệu, thì ta thật sự không mong muốn điều này.

- Đói (Starvation): Nó xảy ra khi một tiến trình không nhận được dịch vụ để xử lý.

- Bế tắc (Deadlock): Xảy ra khi hai tiến trình bị chặn và do đó không tiến trình nào có thể tiến hành xử lý được.

Miền ứng dụng .NET

Trong các tệp thực thi .NET không được lưu trữ trực tiếp trong một tiến trình Windows mà các tệp thực thi được lưu trữ bởi một phân vùng hợp lý trong một tiến trình được gọi là miền ứng dụng (application domain).

Lợi ích:

- AppDomains là một khía cạnh quan trọng trong bản chất trung lập với hệ điều hành của nền tảng .NET Core, với điều kiện là sự phân chia logic này sẽ loại bỏ những khác biệt về cách một hệ điều hành cơ bản thể hiện một tệp thực thi đã tải.

- AppDomains ít tốn kém hơn nhiều về sức mạnh xử lý và bộ nhớ so với một tiến trình toàn diện. Do đó, CoreCLR có thể tải và hủy tải các miền ứng dụng nhanh hơn nhiều so với tiến trình chính thức và có thể cải thiện đáng kể khả năng mở rộng của các ứng dụng máy chủ.

Trong nền tảng .NET, không có sự tương ứng trực tiếp giữa các miền ứng dụng và luồng (thread) mà một AppDomain nhất định có thể có nhiều luồng thực thi bên trong nó tại bất kỳ thời điểm nào.

Để có quyền truy cập theo chương trình vào AppDomain đang lưu trữ chuỗi hiện tại, hãy sử dụng phương thức tĩnh Thread.GetDomain().

Một luồng đơn lẻ cũng có thể được chuyển vào ngữ cảnh thực thi cụ thể tại bất kỳ thời điểm nào và nó có thể được di chuyển trong ngữ cảnh thực thi mới theo ý thích của CoreCLR.

CoreCLR là thực thể chịu trách nhiệm di chuyển các luồng vào (và ra khỏi) bối cảnh thực thi.

Ví dụ demo về các Assemblies trong AppDomain:

using System.Reflection;

namespace ConsoleApp1
{
 internal class Program
 {
  static void Main(string[] args)
  {
    //truy cập domain cho thread hiện thời
   AppDomain defaultAD = AppDomain.CurrentDomain;
   //lấy tất cả các assemply đã được load vào AppDomain mặc định
   Assembly[] loadedAssemplies = defaultAD.GetAssemblies();
   Console.WriteLine($"The assemplies loaded in {defaultAD.FriendlyName}");
   foreach(Assembly assembly in loadedAssemplies )
   {
    Console.WriteLine($"--Name, version: {assembly.GetName().Name}:{assembly.GetName().Version}");
   }
  }
 }
}

Kết quả:

The assemplies loaded in ConsoleApp1
--Name, version: System.Private.CoreLib:6.0.0.0
--Name, version: ConsoleApp1:1.0.0.0
--Name, version: System.Runtime:6.0.0.0
--Name, version: Microsoft.Extensions.DotNetDeltaApplier:17.0.0.0
--Name, version: System.IO.Pipes:6.0.0.0
--Name, version: System.Linq:6.0.0.0
--Name, version: System.Collections:6.0.0.0
--Name, version: System.Console:6.0.0.0

Tương tác với các tiến trình sử dụng .NET

Namespace System.Diagnostics định nghĩa một số loại cho phép bạn tương tác với tiến trình với các quy trình và nhiều loại liên quan đến chẩn đoán, chẳng hạn như nhật ký sự kiện hệ thống và bộ đếm hiệu suất.

Các loại Process-Centric Types của namespace

System.Diagnostics

Mô tả

Process

Cung cấp quyền truy cập vào các tiến trình cục bộ và từ xa, đồng thời cho phép bạn bắt đầu và dừng các tiến trình hệ thống cục bộ.

ProcessModule

Đại diện cho tệp .dll hoặc .exe được tải vào một tiến trình cụ thể.

ProcessModuleCollection

Cung cấp một bộ collection các đối tượng ProcessModule định kiểu mạnh.

ProcessStartInfo

Chỉ định một tập hợp các giá trị được sử dụng khi bạn bắt đầu một tiến trình.

ProcessThread

Đại diện cho một luồng tiến trình của hệ điều hành.

ProcessThreadCollection

Cung cấp một collection các đối tượng ProcessThread định kiểu mạnh.

Lớp System.Diagnostics.Process cho phép chúng ta phân tích các tiến trình đang chạy trên một máy nhất định (cục bộ hoặc từ xa) và cũng cung cấp cho các thành viên khởi động và kết thúc các tiến trình theo chương trình, xem (hoặc sửa đổi) mức độ ưu tiên của tiến trình, và lấy danh sách các luồng đang hoạt động và/hoặc các mô-đun được tải trong một tiến trình nhất định.

Các thuộc tính của lớp Process

Mô tả

ExitTime

Nhận thời gian mà tiến trình liên quan đã thoát

Handle

Lấy xử lý gốc của tiến trình được liên kết

Id

Lấy mã định danh duy nhất cho tiến trình được liên kết

MachineName

Lấy tên của máy tính mà tiến trình liên quan đang chạy trên đó

Modules

Lấy các mô-đun đã được tải bởi tiến trình liên quan

StartTime

Lấy thời gian mà tiến trình liên quan đã được kích hoạt

Các phương thức của lớp Process

Mô tả

CloseMainWindow()

Đóng một tiến trình có giao diện người dùng bằng cách gửi một thông báo đóng tới cửa sổ chính của nó

GetCurrentProcess()

Nhận một thành phần tiến trình mới và liên kết nó với tiến trình hiện đang hoạt động

GetProcesses()

Tạo một thành phần tiến trình mới cho từng tài nguyên tiến trình trên máy tính cục bộ

Kill()

Lập tức dừng tiến trình liên quan

Start()

Bắt đầu (hoặc sử dụng lại) tài nguyên tiến trình được chỉ định bởi thuộc tính StartInfo của thành phần tiến trình này và liên kết nó với thành phần.

Bạn có thể tìm hiểu thêm các thuộc tính và phương thức của lớp Process tại link sau: https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.process?view=net-6.0

Ví dụ demo việc chạy các tiến trình:

using System.Diagnostics;

namespace ConsoleApp1
{
 internal class Program
 {
  static void Main(string[] args)
  {
   int no = 1;
   string info;

   //lấy tất cả các tiến trình trên máy local, được sắp xếp theo PId
   var runningProcs = from proc in Process.GetProcesses(".")
          orderby proc.Id
          select proc;
   //in ra PId và Name của mỗi tiến trình
   foreach (var proc in runningProcs)
   {
    info = $"#{no++}. PID: {proc.Id}\tName: {proc.ProcessName}";
    Console.WriteLine(info);
   }
  }
 }
}

Một ví dụ cho kết quả thực thi:

#1. PID: 0      Name: Idle
#2. PID: 4      Name: System
#3. PID: 124    Name: Registry
#4. PID: 388    Name: chrome
#5. PID: 432    Name: smss
#6. PID: 472    Name: fontdrvhost
#7. PID: 488    Name: WUDFHost
#8. PID: 528    Name: svchost
#9. PID: 580    Name: svchost
#10. PID: 592   Name: csrss

Namespace System.Threading

Namespace System.Threading cung cấp một số kiểu cho phép xây dựng trực tiếp các ứng dụng đa luồng (multithreading).

Nó cung cấp các kiểu cho phép ta tương tác với một luồng CoreCLR cụ thể, namespace này định nghĩa các kiểu cho phép truy cập vào nhóm luồng do CoreCLR duy trì, lớp Timer đơn giản (không dựa trên GUI) và nhiều kiểu được sử dụng để cung cấp quyền truy cập đồng bộ vào tài nguyên được chia sẻ.

Lớp System.Threading.Thread

Kiểu nguyên thủy nhất trong tất cả các kiểu trong namespace System.Threading là Thread.

Thread đại diện cho một trình bao bọc hướng đối tượng xung quanh một đường dẫn thực thi nhất định trong một AppDomain cụ thể.

Nó cũng định nghĩa một số phương thức (cả ở mức độ độ tĩnh và thể hiện) cho phép ta tạo các luồng mới trong AppDomain hiện tại, cũng như tạm dừng, dừng và hủy một luồng cụ thể.

Dưới đây là một ví dụ về Thread:

namespace ConsoleApp1
{
 internal class Program
 {
  static void Main(string[] args)
  {
   //lấy thread hiện thời và đặt tên cho nó
   Thread MainThread = Thread.CurrentThread;
   MainThread.Name = "TheMainThread";

   Console.WriteLine($"ID of current thread: {MainThread.ManagedThreadId}");
   Console.WriteLine($"Thread name: {MainThread.Name}");
   Console.WriteLine($"Has thread start?: {MainThread.IsAlive}");
   Console.WriteLine($"Priority level: {MainThread.Priority}");
   Console.WriteLine($"Thread state: {MainThread.ThreadState}");
  }
 }
}

Dưới đây là một ví dụ cho kết quả thực thi:

ID of current thread: 1
Thread name: TheMainThread
Has thread start?: True
Priority level: Normal
Thread state: Running

Các bước tạo một thread 

1. Tạo một phương thức để làm điểm đầu vào (entry point) cho thread mới

2. Tạo một delegate ParameterizedThreadStart (hoặc ThreadStart) mới, rồi truyền địa chỉ của phương thức được định nghĩa ở bước 1 cho nó

3. Tạo một đối tượng Thread, truyền delegate ParameterizedThreadStart/ThreadStart làm đối số hàm tạo

4. Thiết lập bất kỳ đặc điểm thread ban đầu nào (name, priority, v.v.)

5. Gọi phương thức Thread.Start(). Điều này sẽ kích hoạt thread tại phương thức được tham chiếu bởi delegate được tạo ở bước 2 càng sớm càng tốt.

Ví dụ với delegate ThreadStart:

namespace ConsoleApp1
{
 class Printer
 {
  //bước 1
  public void PrintNumbers()
  {
   //hiển thị thông tin thread
   Console.WriteLine($"{Thread.CurrentThread.Name} đang thực thi phương thức PrintNumbers()");
   //in ra các số
   for(int i = 1; i <= 5; i++)
   {
    Console.WriteLine($"Secondary thread: {i}");
    Thread.Sleep(2000);
   }
  }
 }

 internal class Program
 {
  static void Main(string[] args)
  {
   Thread MainThread = Thread.CurrentThread;
   MainThread.Name = "TheMainThread";
   Console.WriteLine($"{Thread.CurrentThread.Name} đang thực thi phương thức Main()");
   Printer printer = new Printer(); //bước 2
   Thread BackgroundThread = new Thread(new ThreadStart(printer.PrintNumbers)); //bước 3
   BackgroundThread.Name = "Secondary"; //bước 4
   BackgroundThread.Start(); //bước 5

   //làm một vài việc khác
   for(var i = 1; i <=5; i++)
   {
    Console.WriteLine($"Main Thread: {i}");
    Thread.Sleep(1000);
   }
   Console.WriteLine("The main thread has finished");
  }
 }
}

Kết quả:

TheMainThread dang thuc thi phuong thuc Main()
Secondary dang thuc thi phuong thuc PrintNumbers()
Main Thread: 1
Secondary thread: 1
Main Thread: 2
Main Thread: 3
Secondary thread: 2
Main Thread: 4
Secondary thread: 3
Main Thread: 5
The main thread has finished
Secondary thread: 4
Secondary thread: 5

Ví dụ với delegate ParameterizedThreadStart

namespace ConsoleApp1
{
 class PTS
 {
  public int V1 { get; set; }
  public int V2 { get; set; }
 }

 internal class Program
 {
  static AutoResetEvent waitHandle = new AutoResetEvent(false);
  static void AddNumber(object data)
  {
   if(data is PTS p)
   {
    Thread.Sleep(1000);
    Console.WriteLine($"ID of thread in Add(): {Thread.CurrentThread.ManagedThreadId}");
    Console.WriteLine($"{p.V1} + {p.V2} = {p.V1 + p.V2}");
    //báo với thread rằng đã hoàn thành
    waitHandle.Set();
   }
  }

  static void Main(string[] args)
  {
   Console.WriteLine($"ID of thread in Main(): {Thread.CurrentThread.ManagedThreadId}");
   //tạo một đối tượng PTS để truyền tới thread thứ cấp
   PTS pTS = new PTS { V1 = 5, V2 = 10};
   Thread t = new Thread(new ParameterizedThreadStart(AddNumber));
   //thiết lập thread background
   t.IsBackground = true;
   t.Start(pTS);
   //chờ cho việc xử lý wait hoàn thành
   waitHandle.WaitOne();
   Console.WriteLine("Main thread: Done.");
  }
 }
}

Kết quả:

ID of thread in Main(): 1
ID of thread in Add(): 7
5 + 10 = 15
Main thread: Done.

Thread Foreground và Thread Background

Thread Foreground có khả năng ngăn ứng dụng hiện tại chấm dứt. CLR sẽ không tắt một ứng dụng (có nghĩa là hủy tải AppDomain lưu trữ) cho đến khi tất cả các Thread Foreground kết thúc.

Thread Background (đôi khi được gọi là luồng daemon) được CLR xem như các đường dẫn thực thi có thể sử dụng được có thể bỏ qua tại bất kỳ thời điểm nào (ngay cả khi chúng hiện đang xử lý một số đơn vị công việc).

Do đó, nếu tất cả các Thread Foreground đã kết thúc, thì bất kỳ và tất cả các Thread Background sẽ tự động bị hủy khi miền ứng dụng không tải.

Ví dụ về Race condition

namespace ConsoleApp1
{
 class Printer
 {
  //lock token
  private object threadLock = new object();
  public void PrintNumbers()
  {
   //use lock token
   lock(threadLock)
   {
    Monitor.Enter(threadLock);
    try
    {
     Console.WriteLine($"{Thread.CurrentThread.Name} dang thuc thi PrintNumbers()");
     //in ra cac so
     for(var i = 1; i <= 5; i++)
     {
      Random r = new Random();
      Thread.Sleep(500 * r.Next(5));
      Console.Write($"{i,3}{(i == 5 ? "" : ",")}");
     }
     Console.WriteLine();
    }
    catch (Exception ex)
    {
     Console.WriteLine(ex.Message);
    }
    finally
    {
     Monitor.Exit(threadLock);
    }
   }
  }
 }

 internal class Program
 {
  static void Main(string[] args)
  {
   Console.WriteLine("***Demo Synchronizing Threads***\n");
   Printer printer = new Printer();
   //tạo 5 thread đều trỏ tới cùng một phương thức
   //trên cùng một đối tượng.
   Thread[] threads = new Thread[5];
   for(var i = 0; i < 5; i++)
   {
    threads[i] = new Thread(new ThreadStart(printer.PrintNumbers))
    {
     Name = $"Worker thread #{i+1:D2}"
    };
   }
   //start từng thread
   foreach(Thread t in threads)
   {
    t.Start();
   }
  }
 }
}

Kết quả:

***Demo Synchronizing Threads***

Worker thread #01 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
Worker thread #02 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
Worker thread #03 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
Worker thread #04 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
Worker thread #05 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5

Làm việc với TimerCallback

Nhiều ứng dụng có nhu cầu gọi một phương thức cụ thể sau mỗi khoảng thời gian đều đặn:

  • Hiển thị thời gian hiện tại trên thanh trạng thái thông qua chức năng trợ giúp nhất định.
  • Thực hiện các tác vụ nền không quan trọng như kiểm tra các tin nhắn e-mail mới.

Để xử lý vấn đề này ta sử dụng kiểu System.Threading.Timer kết hợp với một delegate có liên quan tên là TimerCallback.

Ví dụ sử dụng TimerCallback:

namespace ConsoleApp1
{
 internal class Program
 {
  static void PrintTime(object state)
  {
   Console.WriteLine($"Time is: {DateTime.Now.ToLongTimeString()}. Param is {state.ToString()}");
  }

  static void Main(string[] args)
  {
   Console.WriteLine("*** Working with Timer type ***");
   //tạo delegate cho Timer
   TimerCallback timerCallback = new TimerCallback(PrintTime);
   //thiết lập các cài đặt cho Timer
   var _ = new Timer(
    timerCallback,     //đối tượng delegate TimerCallbak
    "Hello from Main", //truyền chuỗi tới phương thức được gọi
    0,                 //thời gian chờ trước khi start
    1000               //thời gian lặp giữa các lần gọi
    );
   Console.ReadLine();
  }
 }
}

Kết quả:

*** Working with Timer type ***
Time is: 10:18:41 PM. Param is Hello from Main
Time is: 10:18:42 PM. Param is Hello from Main
Time is: 10:18:43 PM. Param is Hello from Main
Time is: 10:18:44 PM. Param is Hello from Main
Time is: 10:18:45 PM. Param is Hello from Main
Time is: 10:18:46 PM. Param is Hello from Main
Time is: 10:18:47 PM. Param is Hello from Main
Time is: 10:18:48 PM. Param is Hello from Main
Time is: 10:18:49 PM. Param is Hello from Main
Time is: 10:18:50 PM. Param is Hello from Main

Làm việc với ThreadPool

ThreadPool là một nhóm các luồng worker đã được tạo và có sẵn cho các ứng dụng sử dụng chúng khi cần. Khi các luồng của nhóm luồng hoàn thành việc thực thi các tác vụ của chúng, chúng sẽ quay lại nhóm.

ThreadPool quản lý các luồng hiệu quả bằng cách giảm thiểu số lượng luồng phải được tạo, bắt đầu và dừng.

Bằng cách sử dụng threadpool, ta có thể tập trung vào vấn đề business của mình hơn là cơ sở hạ tầng luồng của ứng dụng.

Lớp ThreadPool có một số phương thức tĩnh bao gồm có QueueUserWorkItem chịu trách nhiệm gọi một luồng worker threadpool khi nó khả dụng. Nếu không có luồng worker nào trong threadpool, nó sẽ đợi cho đến khi luồng đó khả dụng.

Ví dụ demo với ThreadPool:

namespace ConsoleApp1
{
 class Printer
 {
  private object threadLock = new object();
  public void PrintNumbers()
  {
   Monitor.Enter(threadLock);
   try
   {
    Console.WriteLine($"->{Thread.CurrentThread.ManagedThreadId} dang thuc thi PrintNumbers()");
    //in ra cac so
    for (var i = 1; i <= 5; i++)
    {
     Random r = new Random();
     Thread.Sleep(500 * r.Next(5));
     Console.Write($"{i,3}{(i == 5 ? "" : ",")}");
    }
    Console.WriteLine();
   }
   catch (Exception ex)
   {
    Console.WriteLine(ex.Message);
   }
   finally
   {
    Monitor.Exit(threadLock);
   }
  }
 }

 internal class Program
 {
  static void PrintNumbers(object state)
  {
   Printer task = (Printer)state;
   task.PrintNumbers();
  }

  static void Main(string[] args)
  {
   Console.WriteLine("*** Demo The CoreCLR Thread Pool ***");
   Console.WriteLine($"Main thread start. ThreadID = {Thread.CurrentThread.ManagedThreadId}");
   Printer p = new Printer();
   WaitCallback workItem = new WaitCallback(PrintNumbers);
   //hàng đợi phương thức 10 lần
   for (var i = 0; i < 10; i++)
   {
    ThreadPool.QueueUserWorkItem(workItem, p);
   }
   Console.WriteLine("All task queued.");
   Console.ReadLine();
  }
 }
}

Kết quả demo:

*** Demo The CoreCLR Thread Pool ***
Main thread start. ThreadID = 1
All task queued.
->6 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->9 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->7 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->8 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->10 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->11 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->12 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->15 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->16 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
->17 dang thuc thi PrintNumbers()
  1,  2,  3,  4,  5
» Tiếp: I/O File
« Trước: Đa luồng (MultiThreading)
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 !!!