- Lập trình bất đồng bộ
- Code đồng bộ
- Lớp Task
- async và await
- Hàm async trả về Task
- Sử dụng CancellationToken - dừng task đang chạy
Lập trình bất đồng bộ asynchronous
Từ .NET Framework 4.5 nó thêm vào thư viện có tên Task Parallel Library (TPL)
- TPL giúp lập trình chạy song song (đa luồng) dễ dàng hơn.
Trong C# đồng thời nó thêm vào hai từ khóa là async
và await
,
đây là hai từ khóa chính để sử dụng trong lập trình bất đồng bộ.
Lập trình bất đồng bộ (asynchronous) là một cách thức mà khi gọi nó chạy ở chế độ nền (liên quan đến một tiến trình, task), trong khi đó tiến trình gọi nó không bị khóa - block. Trong .NET có triển khai một số mô hình lập trình bất đồng bộ như Asynchronous pattern, mẫu bất đồng bộ theo sự kiện và theo tác vụ (TAP - task-based asynchronous pattern)
Phần này sẽ nói về TAP - task-based asynchronous pattern - mộ hình lập trình bất đồng bộ thông dụng trên .NET hiện nay.
Lập trình đồng bộ synchronous
Bình thường, khi lập trình gọi một phương thức nào đó thì phương thức đó chạy và kết thúc thì các dòng code tiếp theo sau lời gọi phương thức đó mới được thực thi, đó là chạy đồng bộ, có nghĩa là thread gọi phương thức bị khóa lại cho đến khi phương thức kết thúc.
Thử xem ví dụ đơn giản sau:
DownloadWebsite01.cs
using System; using System.Net; using System.Threading; namespace CS021_ASYNCHRONOUS { public class DownloadWebsite01 { public static string DownloadWebpage (string url, bool showresult) { using (var client = new WebClient ()) { Console.Write ("Starting download ..."); string content = client.DownloadString (url); Thread.Sleep (3000); if (showresult) Console.WriteLine (content.Substring (0, 150)); return content; } } public static void TestDownloadWebpage() { string url = "https://code.visualstudio.com/"; DownloadWebpage(url, true); Console.WriteLine("Do somthing ..."); } } }
Hãy gọi TestDownloadWebpage()
trong hàm main
để kiểm tra.
Phương thức DownloadWebpage
sử dụng lớp WebClient
để tải về một trang web, trả về chuỗi nội dung trang.
Khi chạy, thì lời gọi DownloadWebpage(url, true);
, phương thức này thi hành xong thì dòng code
Console.WriteLine("Do somthing ...");
mới được thi hành.
Vấn đề là khi DownloadWebpage(url, true);
chạy, nó sẽ khóa thread gọi nó,
làm cho các dòng code tiếp theo phải chờ, nếu hàm đó thi hành mất nhiều thời gian
(đặc biệt là các thao tác đọc stream - đọc file, kết nối web, kết nối CSDL ...)
- trong khi tài nguyên vẫn đủ để làm các việc khác
- thì chương trình vẫn cứ phải chờ phương thức trên kết thúc thì mới thi hành được tác vụ khác
- đặt biệt là khi gọi phương thức trong các tiến trình UI, giao diện người dùng không tương tác được.
Để giải quyết vấn đề này, trong khi chờ cho DownloadWebpage(url, true)
thi hành xong, chương trình vẫn thi hành được các tác vụ khác thì cần đến kỹ thuật
lập trình bất đồng bộ (trước đây gọi là lập trình đa tiến trình, đa luồng)
Lớp Task
Lớp Task
nó biểu thị tác vụ bất đồng bộ, từ đó ta chạy được code bất đồng bộ.
Nếu tác vụ bất đồng bộ đó thi hành xong có kiểu trả về thì dùng Task<T>
với T là kiểu trả về.
Cần sử dụng các namespace sau để có thư viện về Task
using System.Threading; using System.Threading.Tasks;
Chú ý hãy tìm hiểu về hàm ủy quyền delegate C# trước
Cú pháp tạo ra đối tượng Task cơ bản
Để tạo ra một Task bạn cần tham số là một hàm delegate
(
Func hoặc Action), ví dụ delegate có tên myfunc
thì
khởi tạo
-
Nếu myfunc trả về kiểu T (tức là một Func) thì khởi tạo
Task<T> task = new Task<T>(myfunc);
-
Nếu cần truyền tham số cho myfunc thì khởi tạo:
// object là đối tượng tham số truyền cho myfunc Task<T> task = new Task<T>(myfunc, object);
-
Nếu myfunc không trả về giá trị (tức là Action) thì khởi tạo:
// object là đối tượng tham số truyền cho myfunc Task task = new Task(myfunc);
Để chạy Task gọi phương thức Start()
của đối tượng được tạo ra. Nếu
có kết quả trả về thì đọc kết quả tại thuộc tính Result
, đề chờ cho task hoàn
thành thì gọi Wait()
TestAsync01.cs
using System; using System.Net; using System.Threading; using System.Threading.Tasks; namespace CS021_ASYNCHRONOUS { public class TestAsync01 { // Viết ra màn hình thông báo có màu public static void WriteLine (string s, ConsoleColor color) { Console.ForegroundColor = color; Console.WriteLine (s); } // Tạo và chạy Task, sử dụng delegate Func (có kiểu trả về) public static Task<string> Async1 (string thamso1, string thamso2) { // tạo biến delegate trả về kiểu string, có một tham số object Func<object, string> myfunc = (object thamso) => { // Đọc tham số (dùng kiểu động - xem kiểu động để biết chi tiết) dynamic ts = thamso; for (int i = 1; i <= 10; i++) { // Thread.CurrentThread.ManagedThreadId trả về ID của thread đạng chạy WriteLine ($"{i,5} {Thread.CurrentThread.ManagedThreadId,3} Tham số {ts.x} {ts.y}", ConsoleColor.Green); Thread.Sleep (500); } return $"Kết thúc Async1! {ts.x}"; }; Task<string> task = new Task<string> (myfunc, new { x = thamso1, y = thamso2 }); task.Start(); // chạy thread // Làm gì đó sau khi chạy Task ở đây Console.WriteLine("Async1: Làm gì đó sau khi task chạy"); return task; } // Tạo và chạy Task, sử dụng delegate Action (không kiểu trả về) public static Task Async2 () { Action myaction = () => { for (int i = 1; i <= 10; i++) { WriteLine ($"{i,5} {Thread.CurrentThread.ManagedThreadId,3}", ConsoleColor.Yellow); Thread.Sleep (2000); } }; Task task = new Task (myaction); task.Start(); // Làm gì đó sau khi chạy Task ở đây Console.WriteLine("Async2: Làm gì đó sau khi task chạy"); return task; } } }
Chạy trong hàm main
Console.WriteLine($"{' ',5} {Thread.CurrentThread.ManagedThreadId,3} MainThread"); Task<string> t1 = TestAsync01.Async1("A", "B"); Task t2 = TestAsync01.Async2(); Console.WriteLine("Làm gì đó ở thread chính sau khi 2 task chạy"); // Chờ t1 kết thúc và đọc kết quả trả về t1.Wait(); String s = t1.Result; TestAsync01.WriteLine(s, ConsoleColor.Red); // Ngăn không cho thread chính kết thúc // Nếu thread chính kết thúc mà t2 đang chạy nó sẽ bị ngắt Console.ReadKey();
Kết quả chạy
Để khởi tạo một Task
bạn cần tham số là một delegate (Func hoặc Action) - nên cần nắm rõ về
delegate Func và Action trước.
Lưu ý: sử dụng Task<T>
và Func<T,V>
: khi khởi tạo một task cần tham số
là một delegate, khi task chạy xong thì có kết quả trả về nên dùng đến delegate dạng Func<T,V>
,
đầu tiên tạo Delegate dạng này với cú pháp như sau (trường hợp trong Async1 ở trên)
Func<object, return_type> func = (object thamso) => { // code ... return ...; };
Sau đó tạo Task với cú pháp
Task<return_type> task = new Task<return_type>(func, thamso);
Khi đã có Task, gọi phương thức Start()
của nó để bắt đầu chạy thread, một thread mới bất đồng bộ sẽ
khởi chạy - nó sẽ không khóa thread gọi nó - các dòng code tiếp theo của thread chính vẫn chạy trong khi Task đang thi hành.
Khi Task chạy xong, kết quả delegate trả về lưu ở thuộc tính Result
(task.Result)
Kết quả chạy code trên, hai thread con đang chạy song song, trong khi thread chính đàng chờ người người dùng bấm bàn phím.
Cũng để ý Async2
ở trên lại dùng Task chứ không phải Task<T>, áp dụng khi hàm bất đồng bộ không cần
kết quả trả về, lúc đó lại dùng hàm delegate dạng Action
chứ không dùng Func
, cú pháp:
Action action = () => {}; Task task = new Task(action);
async và await
Mã trên bạn thấy, khi đối tượng Task khởi chạy bằng Start
thì thread của Task chạy, và những dòng code
sau Start() được xử lý mà không bị khóa lại. Như đã nói trên, thread chạy - và khi delegate hoàn thành kết quả trả về
lưu ở task.Result
Vấn đề là khi truy cập task.Result
để đọc kết quả trả về, thì dòng code đó sẽ chờ cho Task hoàn thành
để đọc dẫn đến dòng code sau đó không được thực thi - do thread gọi lại bị block, ví dụ cập nhật hàm Async1
để thấy rõ điều đó.
public static Task<string> Async1 (string thamso1, string thamso2) { // tạo biến delegate trả về kiểu string, có một tham số object Func<object, string> myfunc = (object thamso) => { // Đọc tham số (dùng kiểu động - xem kiểu động để biết chi tiết) dynamic ts = thamso; for (int i = 1; i <= 10; i++) { // Thread.CurrentThread.ManagedThreadId trả về ID của thread đạng chạy WriteLine ($"{i,5} {Thread.CurrentThread.ManagedThreadId,3} Tham số {ts.x} {ts.y}", ConsoleColor.Green); Thread.Sleep (500); } return $"Kết thúc Async1! {ts.x}"; }; Task<string> task = new Task<string> (myfunc, new { x = thamso1, y = thamso2 }); task.Start(); // chạy thread // Làm gì đó sau khi chạy Task ở đây Console.WriteLine("Async1: Làm gì đó sau khi task chạy"); string ketqua= task.Result; // khóa (block) thread cha - chờ task hoàn thành Console.WriteLine("Làm gì đó khi task đã kết thúc"); return task; }
Khi Async1
được gọi 1 từ thread chính, nó khác trường hợp
trước - thread chạy nhưng hàm không trả về ngay lập tức - dẫn đến
thread bị khóa - Async2
không được chạy cho đến khi thread trong Async1
kết thúc.
Bạn thấy thread trong Async1
kết thức 2, thì Async2
mới được gọi 3, lúc đó task trong nó mới chạy 4
Vậy 2 task trong ASync1 và Async2 không được chạy đồng thời, task này kết thúc mới chạy được task kia => Lợi ích của đa luồng, bất đồng bộ mất đi.
Giờ bạn mong muốn khi gọi Async1()
, nó trả về ngay lập tức (nghĩa là không khóa thread gọi nó,
mặc dù bắt đầu chạy một task) - trong khi bên trong Async1()
vẫn đảm bảo, có những đoạn code chỉ được thi
hành khi task trong nó kết thúc (đoạn code phía sau string ketqua = task.Result;
)
Lúc này bạn cần sử dụng đến cặp từ khóa async
và await
. Tiến hành làm như sau:
Bước 1) Thêm vào khai báo tên hàm từ khóa async, nó cho trình biên dịch biết đây là hàm bất đồng bộ - khi gọi nó - nó trả về ngay lập tức
public static async Task<string> Async1 (string thamso1, string thamso2)
Bước 2) Trong thân của Async1, phải có đoạn code chờ task hoàn thành
await task; // đây là điểm không khóa thread chính, thread chính chạy tiếp, task chạy tiếp
await
này có ý nghĩa- Lời gọi hàm Async1 chuyển hướng về chỗ gọi nó khi gặp
await
(tạm dừng thi hành mã sau await) - Code trong Async1 phía sau
await
chỉ được chạy khitask
chạy xong - Khi
await
hoàn thành thì nó chứa kết quả của Task nếu có await
chỉ viết được trong hàm có khai báoasync
- Nhớ là
await
phải dùng với Task
Với hàm async không cần kiểu trả về thì phải trả về Task
tránh dùng void
, xem code Async2
phía dưới.
Vậy hàm Async sau khi chuyển nó là hàm bất đồng bộ với từ khóa async
và await
sẽ như sau:
using System; using System.Net; using System.Threading; using System.Threading.Tasks; namespace CS021_ASYNCHRONOUS { public class TestAsyncAwait { // Viết ra màn hình thông báo có màu public static void WriteLine (string s, ConsoleColor color) { Console.ForegroundColor = color; Console.WriteLine (s); } // Tạo và chạy Task, sử dụng delegate Func (có kiểu trả về) public static async Task<string> Async1 (string thamso1, string thamso2) { // tạo biến delegate trả về kiểu string, có một tham số object Func<object, string> myfunc = (object thamso) => { // Đọc tham số (dùng kiểu động - xem kiểu động để biết chi tiết) dynamic ts = thamso; for (int i = 1; i <= 10; i++) { // Thread.CurrentThread.ManagedThreadId trả về ID của thread đạng chạy WriteLine ($"{i,5} {Thread.CurrentThread.ManagedThreadId,3} Tham số {ts.x} {ts.y}", ConsoleColor.Green); Thread.Sleep (500); } return $"Kết thúc Async1! {ts.x}"; }; Task<string> task = new Task<string> (myfunc, new { x = thamso1, y = thamso2 }); task.Start(); // chý ý dòng này, để đảm bảo task được kích hoạt await task; // Từ đây là code sau await (trong Async1) sẽ chỉ thi hành khi task kết thúc TestAsync01.WriteLine("Async1 - làm gì đó khi task chạy xong", ConsoleColor.Red); string ketqua= task.Result; // Đọc kết quả trả về của task - không phải lo block thread gọi Async1 Console.WriteLine(ketqua); // In kết quả trả về của task return ketqua; } public static async Task Async2 () { Action myaction = () => { for (int i = 1; i <= 10; i++) { WriteLine ($"{i,5} {Thread.CurrentThread.ManagedThreadId,3}", ConsoleColor.Yellow); Thread.Sleep (2000); } }; Task task = new Task (myaction); task.Start(); await task; // Làm gì đó sau khi chạy Task ở đây Console.WriteLine("Async2: Làm gì đó sau khi task kết thúc"); // Không cần trả về vì gốc đồng bộ trả về void, đồng bộ sẽ trả về Task } } }
Chạy thử đoạn code trong hàm main
static async Task Main(string[] args) { var t1 = TestAsyncAwait.Async1("x", "y"); var t2 = TestAsyncAwait.Async2(); // Làm gì đó khi t1, t2 đang chạy Console.WriteLine("Task1, Task2 đang chạy"); await t1; // chờ t1 kết thúc Console.WriteLine("Làm gì đó khi t1 kết thúc"); await t2; // chờ t2 kết thúc }
Nhớ là trong hàm main để thi hành được tác vụ chờ task, gọi các phương thức bất đồng bộ, bạn cũng phải thiết lập main là bất đồng bộ như trên.
Sau khi chạy, bạn sẽ thấy 2 task đã cùng chạy một lúc - và đoạn code sau await
trong phương thức
async
chỉ thi hành khi task trong nó kết thúc.
Phương thức async trả về Task
Khi khai báo hàm với async
nến tránh dùng kiểu trả về void
(dù được phép, không await được)
mà hãy dùng Task
nếu không có kiểu trả về hoặc Task<T>
khi có kiểu trả về T. Khi có kiểu
trả về thì trong thân hàm phải có đoạn lệnh return
trả về dữ liệu kiểu T.
Cú pháp
async Task<T> funtion_name<T>(/*tham số nếu có */) { await ... // phải có đoạn code await một Task nào đó return t; // t phải có kiểu T, mặc dù viết return t; nhưng thực tế trình biên dịch tạo ra đối tượng Task<T> để trả về }
Ví dụ khai báo hàm async trả về kiểu Task<int>
thì phải có return
một giá trị hay biến kiểu int
,
khai báo async
đảm bảo trình biên dịch trả về kiểu Task<int>
mặc dù trong thân là return
biểu thức kiểu int
Nhìn lại hàm Async1
trả về kiểu Task<string>
static async Task<string> Async1(string thamso1, string thamso2) { // ... await task; //... return ketqua; // trả về kiểu string }
Do Async1 một Task, thì nó có thể await
ở một phương thức async khác thì phương thức đó cũng
phải là async
như hàm Main ở trên
static async Task Main(string[] args) { var t1 = TestAsyncAwait.Async1("x", "y"); //.. await t1; // chờ t1 kết thúc //... }
Áp dùng xây dựng chức năng tải một file từ internet về có áp dụng
kỹ thuật bất đồng bộ (async/await), dùng WebClient để tải file
Mặc dù WebClient có sẵn phương thức đồng bộ, nhưng ở đây ta sẽ dùng phương thức DownloadData
để tải mảng dữ liệu (file) về, sau đó đưa toàn bộ code thành bất đồng bộ.
Ban đầu để tải được file, dùng code đồng bộ thì sẽ như sau:
public static void DownloadFile (string url) { using (var client = new WebClient ()) { Console.Write ("Starting download ..." + url); // mảng byte tải về byte[] data = client.DownloadData(new Uri(url)); // Lấy tên file để lưu string filename = System.IO.Path.GetFileName(url); System.IO.File.WriteAllBytes(filename, data); } }
Giờ ta sẽ chuyển thành code đồng bộ, nó sẽ có dạng:
DownloadAsync.cs
using System; using System.Net; using System.Threading.Tasks; namespace CS021_ASYNCHRONOUS { public class DownloadAsync { public static async Task DownloadFile (string url) { Action downloadaction = () => { using (var client = new WebClient ()) { Console.Write ("Starting download ..." + url); // mảng byte tải về byte[] data = client.DownloadData(new Uri(url)); // Lấy tên file để lưu string filename = System.IO.Path.GetFileName(url); System.IO.File.WriteAllBytes(filename, data); } }; Task task = new Task(downloadaction); task.Start(); await task; Console.WriteLine("Đã hoàn thành tải file"); } } }
Ta thấy toàn bố code của thân hàm khi ở trạng thái đồng bộ được đưa vào một Action (deleage) có tên
downloadaction
, sau đó tạo Task từ delegate này. Đồng thời biến đổi DownloadFile thành bất đồng bộ.
Hàm Main gọi tải thử
static async Task Main(string[] args) { string url = "https://github.com/microsoft/vscode/archive/1.48.0.tar.gz"; var taskdonload = DownloadAsync.DownloadFile(url); //.. Console.WriteLine("Làm gì đó khi file đang tải"); //.. await taskdonload; Console.WriteLine("Làm gì đó khi file tải xong"); }
Hãy chạy thử để xem kết quả, nó tải về mã nguồn VSC từ github. Trong khi file đang tải ở thread chính làm việc gì đó tùy bạn
Yêu cầu kết thúc Task đang thực thi với CancellationToken
Khi tạo ra các Task, còn có thể khởi tạo - truyền cho nó một đối tượng kiểu CancellationToken,
đối tượng này được phát sinh bởi lớp CancellationTokenSource. Khi trong task có
CancellationToken thì trong quá trình thực thi nó có thể kiểm tra
CancellationToken.IsCancellationRequested
, nếu là true thì có yêu cầu dừng task, lúc
này code trong task chủ động kết thúc task.
Ví dụ: có task1, task2 đang chạy, nếu nhấn phím e thì 2 task này kết thúc
static async Task Main(string[] args) { // Đối tượng để phát đi yêu cầu dừng Task var tokenSource = new CancellationTokenSource(); // Lấy token - để sử dụng bởi task, khi task thực thi // token.IsCancellationRequested là true nếu có phát yêu cầu dừng // bằng cách gọi tokenSource.Cancel var token = tokenSource.Token; // Tạo task1 có sử dụng CancellationToken Task task1 = new Task( () => { for (int i = 0; i < 10000; i++) { // Kiểm tra xem có yêu cầu dừng thì kết thúc task if (token.IsCancellationRequested) { Console.WriteLine("TASK1 STOP"); token.ThrowIfCancellationRequested(); return; } // Chạy tiếp Console.WriteLine("TASK1 runing ... " + i); Thread.Sleep(300); } }, token ); // Tạo task1 có sử dụng CancellationToken Task task2 = new Task( () => { for (int i = 0; i < 10000; i++) { if (token.IsCancellationRequested) { Console.WriteLine("TASK1 STOP"); token.ThrowIfCancellationRequested(); return; } Console.WriteLine("TASK2 runing ... " + i); Thread.Sleep(300); } }, token ); // Chạy các task task1.Start(); task2.Start(); while (true) { var c = Console.ReadKey().KeyChar; // Nếu bấm e sẽ phát yêu cầu dừng task if (c == 'e') { // phát yêu cầu dừng task tokenSource.Cancel(); break; } } Console.WriteLine("Các task đã kết thúc, bấm phím bất kỳ kết thúc chương trình"); Console.ReadKey(); }
Lưu ý trong .NET hỗ trọng dừng task bằng cách dụng CancellationToken, nhưng không có sẵn các lớp hỗ trợ để PAUSE (tạm dừng) task đang chạy, cũng như RESUME (chạy tiếp) một task đang dừng. Nếu muốn có chức năng này bạn cần tự triển khai, hãy theo hướng dẫn tại pausing async methods để xây dựng ra các lớp PauseTokenSource, PauseToken
Mã nguồn ví dụ CS021_ASYNCHRONOUS (git) hoặc tải về ex021