Nội dung

Nội dung

Multithreading(Đa luồng) vs Asynchronous Programming(Bất đồng bộ) vs Parallel Programming(Song song) trong C#

Sê-ri - Lập trình C#
Nội dung

Trong bài viết này tôi sẽ chỉ ra sự khác biệt giữa Multithreading(Đa luồng), Parallel Programming(Lập trình Song song) và Asynchronous Programming(Lập trình Bất đồng bộ) trong C# với các ví dụ trong bài viết này. Những điểm cần ghi nhớ trước khi tiếp tục:

  1. Multithreading(Đa luồng): Là việc một tiến trình được chia thành nhiều luồng.
  2. Parallel Programming(Lập trình Song song): Là việc nhiều tác vụ chạy trên nhiều nhân xử lý cùng lúc.
  3. Asynchronous Programming(Lập trình Song song): Là việc một luồng duy nhất khởi chạy nhiều tác vụ mà không chờ từng cái hoàn thành.

Đa luồng trong C# đề cập đến khả năng tạo và quản lý nhiều luồng trong một tiến trình duy nhất. Một luồng là đơn vị nhỏ nhất của thực thi trong một tiến trình, và nhiều luồng có thể chạy đồng thời, chia sẻ cùng tài nguyên của tiến trình cha nhưng thực thi các luồng mã khác nhau. Để hiểu rõ hơn, vui lòng xem sơ đồ sau.

Multithreading(Đa luồng) trong C# là gì?

  • Mỗi ứng dụng C# bắt đầu với một luồng duy nhất, gọi là luồng chính (main thread).
  • Thông qua .NET framework, C# cung cấp các lớp và phương thức để tạo và quản lý các luồng bổ sung.
  • Các lớp chính để quản lý luồng trong C# nằm trong namespace System.Threading.
  • Thread: Đại diện cho một luồng đơn. Nó cung cấp các phương thức và thuộc tính để kiểm soát và truy vấn trạng thái của một luồng.
  • ThreadPool: Cung cấp một nhóm các luồng công nhân có thể được dùng để thực thi tác vụ, đăng công việc, và xử lý các thao tác I/O bất đồng bộ.
  • Cải thiện độ phản hồi: Trong các ứng dụng giao diện, một tác vụ lâu có thể được chuyển sang luồng riêng để giữ cho giao diện người dùng không bị treo.
  • Tối ưu tài nguyên: Cho phép sử dụng CPU hiệu quả hơn, nhất là trên các bộ xử lý đa nhân.
  • Điều kiện tranh chấp (Race Conditions): Xảy ra khi hai luồng cùng truy cập và cố thay đổi dữ liệu chia sẻ cùng lúc.
  • Deadlocks: Xảy ra khi hai hoặc nhiều luồng chờ nhau giải phóng tài nguyên, dẫn đến bế tắc.
  • Thiếu tài nguyên: Xảy ra khi một luồng liên tục bị từ chối tài nguyên và không thể tiếp tục công việc.

Để xử lý các thách thức này, sử dụng các primitive đồng bộ hóa như Mutex, Monitor, Semaphore, và từ khóa lock trong C#.

  • Tạo quá nhiều luồng có thể làm giảm hiệu năng do chi phí chuyển ngữ cảnh.
  • Luồng tiêu tốn tài nguyên, nên sử dụng quá mức sẽ làm giảm hiệu suất và độ phản hồi.
  • Đồng bộ hóa có thể gây thêm chi phí, nên cần cân bằng hợp lý.
using System.Threading;
using System;
namespace ThreadingDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main Thread Started");

            //Creating Threads
            Thread t1 = new Thread(Method1)
            {
                Name = "Thread1"
            };
            Thread t2 = new Thread(Method2)
            {
                Name = "Thread2"
            };
            Thread t3 = new Thread(Method3)
            {
                Name = "Thread3"
            };

            //Executing the methods
            t1.Start();
            t2.Start();
            t3.Start();
            Console.WriteLine("Main Thread Ended");
            Console.Read();
        }
        static void Method1()
        {
            Console.WriteLine("Method1 Started using " + Thread.CurrentThread.Name);
            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine("Method1 :" + i);
            }
            Console.WriteLine("Method1 Ended using " + Thread.CurrentThread.Name);
        }

        static void Method2()
        {
            Console.WriteLine("Method2 Started using " + Thread.CurrentThread.Name);
            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine("Method2 :" + i);
                if (i == 3)
                {
                    Console.WriteLine("Performing the Database Operation Started");
                    //Sleep for 10 seconds
                    Thread.Sleep(10000);
                    Console.WriteLine("Performing the Database Operation Completed");
                }
            }
            Console.WriteLine("Method2 Ended using " + Thread.CurrentThread.Name);
        }
        static void Method3()
        {
            Console.WriteLine("Method3 Started using " + Thread.CurrentThread.Name);
            for (int i = 1; i <= 5; i++)
            {
                Console.WriteLine("Method3 :" + i);
            }
            Console.WriteLine("Method3 Ended using " + Thread.CurrentThread.Name);
        }
    }
}

Đa luồng là một tính năng mạnh mẽ, nhưng cần thiết kế và kiểm thử cẩn thận. Việc giới thiệu lớp Task trong các phiên bản C# sau này đã đơn giản hóa nhiều kịch bản đa luồng, làm mờ ranh giới giữa đa luồng và lập trình bất đồng bộ, đồng thời cung cấp cách tiếp cận dễ và hiệu quả hơn để thực thi đồng thời.


Lập trình bất đồng bộ trong C# là phương pháp thực hiện các tác vụ mà không chặn luồng chính hoặc luồng gọi. Điều này đặc biệt hữu ích cho các thao tác I/O (như đọc tệp, lấy dữ liệu từ web, truy vấn cơ sở dữ liệu), nơi việc chờ tác vụ hoàn tất có thể lãng phí thời gian CPU có thể được dùng cho công việc khác. Để hiểu rõ hơn, vui lòng xem sơ đồ sau.

Asynchronous Programming(Lập trình Bất đồng bộ) trong C# là gì?

Như bạn thấy trong hình trên, khi có một request đến server, server sẽ sử dụng một luồng từ Thread Pool và bắt đầu thực thi code ứng dụng. Nhưng điểm quan trọng là nếu luồng này thực hiện một thao tác I/O, nó sẽ không chờ cho đến khi I/O hoàn tất, tức là luồng sẽ không bị block; nó sẽ trả lại Thread Pool và có thể phục vụ request khác. Nhờ đó CPU được sử dụng hiệu quả hơn, xử lý nhiều request hơn và cải thiện hiệu năng tổng thể ứng dụng.

C# và .NET cung cấp hỗ trợ hạng nhất cho lập trình bất đồng bộ, giúp việc viết code không chặn trở nên đơn giản hơn cho lập trình viên. Dưới đây là tổng quan:

  • async và await là hai từ khóa chính được giới thiệu trong C# 5.0 để đơn giản hóa lập trình bất đồng bộ. Khi một phương thức được đánh dấu async, nó có thể dùng await để gọi các phương thức trả về Task hoặc Task. await sẽ báo cho trình biên dịch: “Nếu tác vụ chưa xong, hãy để phần khác chạy cho tới khi xong.”
  • Task: Đại diện cho một thao tác bất đồng bộ có thể await. Nằm trong namespace System.Threading.Tasks.
  • Task: Đại diện cho một thao tác bất đồng bộ trả về giá trị kiểu T.
  • TaskCompletionSource: Cho phép tạo thao tác bất đồng bộ không dựa vào async/await.
  • Độ phản hồi: Trong ứng dụng UI, sử dụng các hàm bất đồng bộ giúp giao diện không bị đơ vì luồng UI không bị chặn.
  • Khả năng mở rộng: Trong ứng dụng server, các thao tác bất đồng bộ giúp tăng throughput nhờ giải phóng luồng.
  • Sử dụng async/await không đồng nghĩa với việc dùng đa luồng. Nó là về sử dụng luồng hiệu quả.
  • Tránh async void vì không await được, ngoại lệ ném ra cũng không bắt ngoài được. Chủ yếu dùng cho event handler.
  • Code bất đồng bộ đôi khi khó debug và lý giải hơn, nhất là khi phối hợp nhiều tác vụ hoặc xử lý ngoại lệ.
using System;
using System.Threading.Tasks;

namespace AsynchronousProgramming
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Main Method Started......");

            var task = SomeMethod();

            Console.WriteLine("Main Method End");

            string Result = task.Result;
            Console.WriteLine($"Result : {Result}");

            Console.WriteLine("Program End");
            Console.ReadKey();
        }

        public async static Task<string> SomeMethod()
        {
            Console.WriteLine("Some Method Started......");

            await Task.Delay(TimeSpan.FromSeconds(2));
            Console.WriteLine("\n");

            Console.WriteLine("Some Method End");
            return "Some Data";
        }
    }
}

Sức mạnh của lập trình bất đồng bộ trong C# nằm ở khả năng cải thiện cả độ phản hồi và khả năng mở rộng, nhưng cần hiểu nguyên lý bên dưới để sử dụng hiệu quả và tránh các cạm bẫy.


Lập trình song song trong C# là quá trình sử dụng tính đồng thời để thực thi nhiều phép tính cùng lúc nhằm cải thiện hiệu suất và độ phản hồi phần mềm. Để hiểu rõ hơn, vui lòng xem sơ đồ bên dưới. Như bạn thấy, cùng một tác vụ sẽ được thực thi bởi nhiều tiến trình, nhiều nhân, mỗi tiến trình có nhiều luồng để chạy code ứng dụng.

Parallel Programming(Lập trình Song song) trong C# là gì?

Trong C# và .NET Framework, lập trình song song chủ yếu được thực hiện thông qua Task Parallel Library (TPL).

  • Data Parallelism: Thực hiện cùng một thao tác đồng thời (song song) trên các phần tử trong một tập dữ liệu hoặc phân vùng dữ liệu.
  • Task Parallelism: Chạy nhiều tác vụ riêng biệt song song. Mỗi task có thể là một thao tác độc lập được thực hiện cùng lúc.
  • System.Threading.Tasks: Namespace chứa các kiểu chính của TPL, gồm Task và Parallel.
  • Parallel: Lớp tĩnh cung cấp các hàm lặp song song như Parallel.For và Parallel.ForEach.
  • PLINQ (Parallel LINQ): Một phiên bản song song của LINQ, cho phép thực hiện các thao tác song song trên tập hợp.
  • Hiệu suất: Nhờ tận dụng nhiều CPU hoặc nhiều nhân, các tác vụ tính toán hoàn thành nhanh hơn.
  • Độ phản hồi: Trong ứng dụng desktop, các phép tính lâu có thể đẩy xuống luồng nền để ứng dụng phản hồi tốt hơn.
  • Overhead: Việc phân chia tác vụ và tổng hợp kết quả gây ra overhead. Không phải tác vụ nào cũng có lợi khi song song hóa.
  • Thứ tự: Các thao tác song song có thể không giữ nguyên thứ tự dữ liệu (đặc biệt với PLINQ). Nếu cần giữ thứ tự, phải bổ sung và có thể làm giảm hiệu năng.
  • Đồng bộ: Khi nhiều tác vụ truy cập dữ liệu chia sẻ, cần cơ chế đồng bộ (lock, Concurrent Collections) để tránh race condition.
  • Max Degree of Parallelism: Có thể giới hạn số lượng tác vụ đồng thời bằng thuộc tính MaxDegreeOfParallelism trong PLINQ.

Lập trình song song và bất đồng bộ đều xử lý concurrency, nhưng mục tiêu chính khác nhau. Lập trình song song tận dụng đa nhân để tăng hiệu suất tính toán, còn lập trình bất đồng bộ là để nâng cao độ phản hồi bằng cách không chờ các tác vụ chạy lâu.

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

namespace ParallelProgrammingDemo
{
    class Program
    {
        static void Main()
        {
            List<int> integerList = Enumerable.Range(1, 10).ToList();

            Console.WriteLine("Parallel For Loop Started");
            Parallel.For(1, 11, number => {
                Console.WriteLine(number);
            });
            Console.WriteLine("Parallel For Loop Ended");

            Console.WriteLine("Parallel Foreach Loop Started");
            Parallel.ForEach(integerList, i =>
            {
                long total = DoSomeIndependentTimeconsumingTask();
                Console.WriteLine("{0} - {1}", i, total);
            });
            Console.WriteLine("Parallel Foreach Loop Ended");

            //Calling Three methods Parallely
            Console.WriteLine("Parallel Invoke Started");
            Parallel.Invoke(
                 Method1, Method2, Method3
            );
            Console.WriteLine("Parallel Invoke Ended");
            Console.ReadLine();
        }

        static long DoSomeIndependentTimeconsumingTask()
        {
            //Do Some Time Consuming Task here
            long total = 0;
            for (int i = 1; i < 100000000; i++)
            {
                total += i;
            }
            return total;
        }

        static void Method1()
        {
            Thread.Sleep(200);
            Console.WriteLine($"Method 1 Completed by Thread={Thread.CurrentThread.ManagedThreadId}");
        }
        static void Method2()
        {
            Thread.Sleep(200);
            Console.WriteLine($"Method 2 Completed by Thread={Thread.CurrentThread.ManagedThreadId}");
        }
        static void Method3()
        {
            Thread.Sleep(200);
            Console.WriteLine($"Method 3 Completed by Thread={Thread.CurrentThread.ManagedThreadId}");
        }
    }
}

Áp dụng lập trình song song hiệu quả trong C# cần hiểu rõ bài toán, bản chất dữ liệu và thao tác, cũng như các thách thức khi chạy đồng thời. Luôn cần đo kiểm và test kỹ để đảm bảo giải pháp song song thực sự hiệu quả hơn giải pháp tuần tự.


  • Định nghĩa: Đa luồng là cho phép một tiến trình quản lý nhiều luồng, là đơn vị nhỏ nhất của ngữ cảnh thực thi CPU. Mỗi luồng có thể chạy bộ lệnh riêng.
  • Trong C#: Sử dụng lớp System.Threading.Thread hoặc ThreadPool để quản lý luồng.
  • Tình huống sử dụng: Phù hợp cho cả tác vụ I/O-bound và CPU-bound. Đặc biệt hiệu quả khi các tác vụ có thể chạy đồng thời mà không cần chờ nhau, hoặc có thể chia nhỏ.
  • Ưu: Sử dụng tốt đa nhân CPU nếu quản lý đúng.
  • Nhược: Ngốn tài nguyên, quản lý thủ công dễ gặp deadlock, race condition, chi phí chuyển ngữ cảnh cao có thể làm giảm hiệu suất.
  • Định nghĩa: Cho phép hệ thống thực hiện tác vụ mà không cần chờ tác vụ trước hoàn thành. Chủ yếu là điều phối luồng thực thi, không phải thực thi đồng thời thật sự.
  • Trong C#: Dùng từ khóa asyncawait (C# 5.0 trở đi), kết hợp với TaskTask.
  • Tình huống sử dụng: Rất phù hợp với I/O-bound (file, database, web request…), nơi không muốn block chờ thao tác hoàn tất.
  • Ưu: Tăng độ phản hồi (UI không bị treo), dễ quản lý hơn quản lý luồng thô.
  • Nhược: Khó debug, tiềm ẩn nhiều lỗi khó kiểm soát, yêu cầu hiểu rõ cơ chế bên dưới.
  • Định nghĩa: Chia nhỏ một tác vụ thành nhiều phần và xử lý đồng thời, thường chạy trên nhiều CPU/core. Tập trung vào thực thi đồng thời các tác vụ (hoặc các phần của một tác vụ).
  • Trong C#: Sử dụng System.Threading.Tasks.Parallel, PLINQ (Parallel LINQ), Parallel.Invoke, Parallel.For/ForEach…
  • Tình huống sử dụng: Phù hợp cho tác vụ CPU-bound, có thể chia nhỏ độc lập, ví dụ: xử lý tập dữ liệu lớn, tính toán phức tạp.
  • Ưu: Tăng tốc rõ rệt khi xử lý khối lượng lớn nhờ tận dụng tối đa tài nguyên hệ thống.
  • Nhược: Không phải mọi tác vụ đều song song hóa được. Việc chia nhỏ, gom kết quả, đồng bộ dữ liệu có thể làm giảm lợi ích.

Các khái niệm này thường có sự chồng lắp.
Ví dụ:

  • Đa luồng và Song song thường đi cùng nhau, vì các tác vụ song song thường được thực thi trên nhiều luồng.
  • Lập trình bất đồng bộ cũng có thể liên quan đến đa luồng khi các tác vụ được đẩy sang thread khác.
  • Tác vụ I/O-bound: Ưu tiên lập trình bất đồng bộ.
  • Tác vụ CPU-bound: Ưu tiên Đa luồng hoặc Song song.

Song song hóa hoặc đa luồng hóa không đồng nghĩa hiệu suất luôn cao hơn. Có thể bị giảm hiệu năng vì overhead và chuyển ngữ cảnh. Luôn cần đo kiểm và hiểu bài toán thật sự.


using System.Threading;
Thread loadDataThread = new Thread(() =>
{
    LoadLargeDataset();
});
loadDataThread.Start();
using System.Timers;
Timer timer = new Timer(10000); // 10 giây
timer.Elapsed += (sender, e) => PollService();
timer.Start();
List<string> fileUrls = GetFileUrls();
foreach (var url in fileUrls)
{
    Thread downloadThread = new Thread(() =>
    {
        DownloadFile(url);
    });
    downloadThread.Start();
}
TcpListener listener = new TcpListener(IPAddress.Any, port);
while (true)
{
    TcpClient client = listener.AcceptTcpClient();
    Thread clientThread = new Thread(() =>
    {
        HandleClient(client);
    });
    clientThread.Start();
}
int[] computationParts = DivideComputation();
Parallel.ForEach(computationParts, part =>
{
    RunComputation(part);
});

using System.Net.Http;

public async Task<string> FetchDataAsync(string url)
{
    using (HttpClient client = new HttpClient())
    {
        return await client.GetStringAsync(url);
    }
}
using System.IO;

public async Task<string> ReadFileAsync(string filePath)
{
    using (StreamReader reader = new StreamReader(filePath))
    {
        return await reader.ReadToEndAsync();
    }
}
public async Task<List<Product>> GetProductsAsync()
{
    using (var dbContext = new MyDbContext())
    {
        return await dbContext.Products.ToListAsync();
    }
}
public async Task DoHeavyWorkAsync()
{
    await Task.Run(() =>
    {
        // Tác vụ nặng
    });
}
public async Task ProcessDataAsync()
{
    string rawData = await FetchDataAsync("https://api.example.com/data");
    List<DataModel> models = await ParseDataAsync(rawData);
    await SaveToDatabaseAsync(models);
}
public async Task ProcessMultipleFilesAsync(List<string> filePaths)
{
    var tasks = filePaths.Select(filePath => ProcessFileAsync(filePath)).ToList();
    await Task.WhenAll(tasks);
}

using System.Threading.Tasks;

var images = LoadImages();
Parallel.ForEach(images, image =>
{
    ApplyFilter(image);
});
var data = Enumerable.Range(0, 10000);
var results = data.AsParallel()
                  .Where(item => IsPrime(item))
                  .Select(item => Compute(item));
using System.Threading.Tasks;

Task task1 = ProcessDataAsync(data1);
Task task2 = ProcessDataAsync(data2);
Task task3 = ProcessDataAsync(data3);

await Task.WhenAll(task1, task2, task3);
using System.Threading.Tasks;

double result = 0.0;
object syncLock = new object();

Parallel.ForEach(data, item =>
{
    double itemResult = Compute(item);
    lock (syncLock)
    {
        result += itemResult;
    }
});
using System.Threading.Tasks;

int[,] matrixA = GetMatrixA();
int[,] matrixB = GetMatrixB();
int[,] result = new int[rows, cols];

Parallel.For(0, rows, i =>
{
    for (int j = 0; j < cols; j++)
    {
        for (int k = 0; k < cols; k++)
        {
            result[i, j] += matrixA[i, k] * matrixB[k, j];
        }
    }
});
using System.Threading.Tasks;
Parallel.ForEach(imageChunks, chunk =>
{
    ApplyFilter(chunk);
});

  • Tăng độ phản hồi ứng dụng: Nhất là ứng dụng UI, mọi thao tác lâu nên đưa xuống luồng nền.
  • Tác vụ CPU-bound có thể chia nhỏ: Chia nhỏ tính toán độc lập và phân cho nhiều luồng.
  • Xử lý đồng thời: Ứng dụng server phục vụ nhiều client, xử lý batch nhiều tác vụ độc lập.
  • Thao tác I/O cũ không hỗ trợ async: Hoặc codebase cũ.
  • Lập lịch công việc: Tác vụ định kỳ.
  • Pooling: Quản lý pool kết nối hoặc pool luồng.
  • Thuật toán chia để trị, tính toán thời gian thực.

  • Tăng phản hồi ứng dụng: Đặc biệt UI/Desktop/Mobile, web app khi thao tác file, db, request API.
  • Tác vụ I/O-bound: Đọc ghi file lớn, request mạng, truy vấn DB.
  • Tăng khả năng mở rộng: Web server, serverless, tối ưu chi phí trên cloud.
  • Chaining thao tác: Kết hợp nhiều thao tác bất đồng bộ nối tiếp.
  • Thư viện hiện đại thường hỗ trợ async.

  • Tác vụ CPU-bound chia nhỏ độc lập: Xử lý tính toán lớn, chạy nhiều sub-task cùng lúc.
  • Data parallelism: Áp dụng thao tác giống nhau cho nhiều phần tử.
  • Task parallelism: Nhiều task riêng biệt chạy song song.
  • Thuật toán song song: Sắp xếp song song, nhân ma trận song song,…
  • Tăng throughput: Xử lý nhiều request, mô phỏng lớn, tìm kiếm trong dữ liệu lớn, batch processing.

  • Multithreading(Đa luồng): Tối ưu tài nguyên, thực thi đồng thời, giữ UI responsive, chia sẻ bộ nhớ.
  • Asynchronous Programming(Lập trình Bất đồng bộ): Không block luồng, tăng phản hồi và khả năng mở rộng, code dễ đọc cho thao tác phức tạp.
  • Parallel Programming(Lập trình Song song): Tối đa hóa CPU, giảm thời gian tính toán, xử lý dữ liệu lớn/phép toán phức tạp.

Tóm lại:

  • Multithreading(Đa luồng): Nhiều luồng hoạt động đồng thời trong 1 tiến trình.
  • Asynchronous Programming(Lập trình Bất đồng bộ): Thực thi không chặn, tập trung vào thao tác I/O.
  • Parallel Programming(Lập trình Song song): Chia nhỏ xử lý đồng thời trên nhiều CPU/core, tập trung cho tác vụ tính toán.

Ba mô hình này có thể kết hợp trong một ứng dụng. Ví dụ: khởi tạo thao tác I/O bất đồng bộ, sau đó xử lý kết quả song song trên nhiều luồng.


Trong bài viết này, tôi đã cố gắng giải thích sự khác biệt giữa Multithreading(Đa luồng), Asynchronous Programming(Lập trình Bất đồng bộ) và Parallel Programming(Lập trình Song song) trong C# cùng các ví dụ. Hy vọng bạn sẽ thích nội dung này.