Thread và Tasks Trong C# Sử Dụng Như Thế Nào?
Lập trình bất đồng bộ và lập trình đa luồng là một tính năng rất quan trọng trong việc lập trình các chương trình lớn và phức tạp hiện nay. Giúp tận dụng tối đa các tài nguyên như CPU, RAM, …, giảm độ trễ của ứng dụng, cải thiện khả năng đáp ứng của ứng dụng.
Trong ngôn ngữ C#, để thực sử dụng lập trình bất đồng bộ ta có thể sử dụng đối tượng Thread trong namespace System.Threading
hoặc đối tượng Tasks trong System.Threading.Tasks
Trước khi tìm hiểu về Task và Thread cũng như là ưu nhược điểm của chúng, đầu tiên chúng ta cần tìm hiểu rõ các khái niệm về Synchronous (đồng bộ) và Asynchronous (bất đồng bộ)
0. Synchronous (đồng bộ) và Asynchronous (bất đồng bộ)
Synchronous (đồng bộ) là một tiến trình xử lý các công việc theo thứ tự đã được định sẵn. Công việc phía sau phải đợi công việc phía trước hoàn thành thì mới bắt đầu thực hiện. Trong lập trình, một chương trình đồng bộ là một chương trình được thực hiện theo từng câu lệnh từ trên xuống dưới, từ trái qua phải, câu lệnh sau chỉ được thực hiện khi câu lệnh phía trước hoàn thành.
Với C#, chương trình bắt đầu thực thi từ hàm Main
và kết thúc khi phương thức Main returns
, đây là luồng chính và chịu trách nhiệm thực thi tất cả các phương thức trong đó. Trong hàm Main, các phương thức được thực thi tuần tự và lần lượt từng cái một.
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
ThreadOne ();
ThreadTwo ();
watch.Stop ();
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static void ThreadOne () {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
}
private static void ThreadTwo () {
Thread.Sleep (2000);
Console.WriteLine ("ThreadTwo");
}
Như đoạn mã trên, ThreadTwo() sẽ không được thực hiện cho tới khi ThreadOne() kết thúc.
Asynchronous (bất đồng bộ), là tiến trình mà việc xử lý các công việc được thực hiện đồng thời cùng lúc. Công việc sau không phải chờ đợi công việc phía trước hoàn thành thì mới bắt đầu thực hiện mà thay vào đó, cả hai công việc sẽ cùng được thực hiện cùng lúc.
Trong ví dụ trên ta thấy ThreadOne() và ThreadTwo() không phụ thuộc vào nhau, và ThreadOne() đang mất nhiều thời gian để hoàn thành nhiệm vụ của nó. Trong lập trình đồng bộ, nó sẽ thực thi phương thức ThreadOne() đầu tiên và nó sẽ đợi hoàn thành phương thức này và sau đó nó sẽ thực thi phương thức ThreadTwo(). Vì vậy, nó sẽ là một quá trình tốn nhiều thời gian mặc dù cả hai phương thức không phụ thuộc vào nhau.
Để tối ưu chương trình ta có thể sử dụng lập trình bất động bộ để thực hiện hai phương thức này một cách đồng thời bằng cách cho mỗi phương thức được thực thi trên một luồng khác nhau. Bằng cách đó, các phương thức được thực thi một cách đồng thời, có nghĩa là nhiều tác vụ được thực thi cùng một lúc.
Để thực hiện các phương thức bất đồng bộ trong c# ta có thể sử dụng Thead hoặc từ khóa async
/await
.
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
// sử dụng Thread để lập trình bất đồng bộ
Thread th_one = new Thread (ThreadOne);
Thread th_two = new Thread (ThreadTwo);
th_one.Start ();
th_two.Start ();
// Chặn luồng tiếp tục cho tới khi các tiến trình th_one và th_two hoàn thành
th_one.Join ();
th_two.Join ();
watch.Stop ();
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static void ThreadOne () {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
}
private static void ThreadTwo () {
Thread.Sleep (2000);
Console.WriteLine ("ThreadTwo");
}
Với việc áp dụng lập trình bất đồng bộ, ThreadOne() và ThreadTwo() sẽ được thực thi cùng nhau.
Như ta thấy với việc lập trình bất đồng bộ như trên, thời gian để chương trình thực thi đã giảm một cách đáng kể.
Đối với đoạn code trên chúng ta sử dụng Thread để lập trình bất đồng bộ, nhưng trong C#, ngoài Thread ra, từ phiên bản Net Framework 4.5 trở đi chúng ta còn có một đối tượng mới hỗ trợ lập trình bất đồng bộ là Task
và có sẵn trong namespace System.Threading.Tasks.
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
var task_one = TaskOne ();
var task_two = TaskTwo ();
Task.WaitAll (task_one, task_one);
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static async Task TaskOne () {
await Task.Delay (5000);
Console.WriteLine ("TaskOne");
}
private static async Task TaskTwo () {
await Task.Delay (2000);
Console.WriteLine ("TaskTwo");
}
1. Thread (luồng) là gì?
Thread là gì?
Theo wikipedia, một thread là một chuỗi các lệnh được lập trình có thể quản lý độc lập bởi bộ lập lịch. Một thread là một thành phần của một tiến trình, một tiến trình thì có thể có nhiều thread, có thể thực thi đồng thời và chia sẻ tài nguyên như CPU, RAM, … trong khi các tiến trình thì không chia sẻ các tài nguyên này.
Tạo một Thread mới.
Để tạo một Thead mới chúng ta sử dụng phương thức Start()
, Thread không bắt đầu chạy cho đến khi chúng ta gọi phương thức Start():
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
// sử dụng Thread để lập trình bất đồng bộ
Thread th_one = new Thread (ThreadOne);
th_one.Start (); // start thread mới
Console.ReadKey ();
}
private static void ThreadOne () {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
}
Hoặc có thể sử dụng biểu thức lamda thay vì phương thức được đặt tên:
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
// sử dụng Thread để lập trình bất đồng bộ
Thread th_one = new Thread (() => {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
});
th_one.Start (); // start thread mới
Console.ReadKey ();
}
Hoặc có thể viết gọn thành:
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
// sử dụng Thread để lập trình bất đồng bộ
new Thread (() => {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
}).Start ();
Console.ReadKey ();
}
Trước khi gọi phương thức Start(), chúng ta có thể một số thuộc tính cho Thread như tên, độ ưu tiên. Việc đặt tên cho Thread sẽ giúp ta gỡ lỗi một cánh dễ dàng hơn nhờ chỉ rõ luồng trong cửa sổ debug Thread.
Truyền tham số với Thread.
Ở trên chúng ta đã làm quen với sử dụng Thread không tham số, để sử dụng tham số trong Thread, chúng ta truyền parameter thông qua phương thức Start().
private static void Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
// sử dụng Thread để lập trình bất đồng bộ
Thread th_one = new Thread (ThreadOne);
th_one.Name = "Thread_One";
th_one.Start ("Tai");
Console.ReadKey ();
}
private static void ThreadOne (object name) {
Thread.Sleep (5000);
Console.WriteLine ("ThreadOne");
Console.WriteLine ("Hello " + (string) name);
}
Một số phương thức thông dụng được sử dụng trong Thread:
Abort()
: khi phương thức này được gọi, hệ thống sẽ ném ra một ngoại lệ ThreadAbortExceptionđể kết thúc thread. Sau khi gọi phương thức này, thuộc tính ThreadState sẽ chuyển sang giá trị Stopped.Suspend()
: phương thức này sẽ tạm dừng việc thực thi của Thread vô thời hạn cho đến khi nó được yêu cầu chạy tiếp tục với phương thức Resume().
Nhược điểm khi sử dụng Thread:
- Sử dụng đa luồng dẫn tới sự phức tạp hóa code, gây khó hiểu khi đọc code. Gây khó khăng trong quá trình debug và test.
- Không thể xác định được khi nào Thread sẽ hoàn thành và giá trị trả về là gì.
2. Tasks sử dụng như thế nào?
Trong .Net Framework 4.5 trở đi, chúng ta có hai từ khóa mới để sử dụng lập trình bất đông bộ là Async
và Await
.
Để sử dụng từ khóa “await
” trong một phương thức, chúng ta cần khai báo phương thức với từ khóa “async”.
private static async Task TaskOne () {
await Task.Delay (5000);
Console.WriteLine ("TaskOne");
}
Khi sử dụng “await
” các phương thức bất đồng bộ được gọi sẽ được thực hiện một cách tuần tự như khi chúng ta lập trình đồng bộ.
private static async Task Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
await TaskOne ();
await TaskTwo ();
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static async Task TaskOne () {
await Task.Delay (5000);
Console.WriteLine ("TaskOne");
}
private static async Task TaskTwo () {
await Task.Delay (2000);
Console.WriteLine ("TaskTwo");
}
Get data return và sử dụng tham số:
Với việc sử dụng Tasks, việc lấy giá trị trả về của một phương thức hoặc truyền parameter cho phương thức được thực hiện một cách dễ dàng, chúng ta cùng xem qua ví dụ dưới:
private static async Task Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
var task_one = await TaskOne ();
var task_two = await TaskTwo ("Tai");
Console.WriteLine ($"Data tasl one: {task_one}");
Console.WriteLine ($"Data tasl two: {task_two}");
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static async Task<string> TaskOne () {
await Task.Delay (5000);
return "TaskOne";
}
private static async Task<string> TaskTwo (string name) {
await Task.Delay (2000);
return "Hello " + name;
}
Để chạy đồng thời hai phương thức và nhận kết quả trả về của hai phương thức, ta có thể sử dụng:
private static async Task Main (string[] args) {
Console.WriteLine ("Thead Vs Async/Await");
var watch = new System.Diagnostics.Stopwatch ();
watch.Start ();
var task_one = TaskOne ();
var task_two = TaskTwo ("Tai");
var results = await Task.WhenAll (task_one, task_two);
Console.WriteLine ($"Data task one: {results[0]}");
Console.WriteLine ($"Data task two: {results[1]}");
Console.WriteLine ($"Execution Time: {watch.ElapsedMilliseconds} ms");
Console.ReadKey ();
}
private static async Task<string> TaskOne () {
await Task.Delay (5000);
return "TaskOne";
}
private static async Task<string> TaskTwo (string name) {
await Task.Delay (2000);
return "Hello " + name;
}
Ưu điểm khi sử dụng Task:
- Có thể nhận được kết quả trả về của phương thức một cách dễ dàng.
- Hỗ trợ phương thức hủy một Tasks thông qua phương thức CancellationTokenSource ().
- Dễ dàng triển khai code bất đồng bộ thông qua từ khóa “async” và “await”.
Tạm kết
Trên đây là một số hướng dẫn cơ bản để có thể dùng Thread và Task để lập trình bất đồng bộ trong C#. Nếu có ý kiến thắc mắc hoặc cần giải đáp bổ sung gì, hãy để lại comment phía dưới nhé các bạn. Chúc các bạn thành công!