Dưới đây là chuỗi các đoạn mã mà tôi đã sử dụng gần đây để minh họa sự khác biệt và các vấn đề khác nhau khi sử dụng giải pháp không đồng bộ.
Giả sử bạn có một số trình xử lý sự kiện trong ứng dụng dựa trên GUI của mình, mất rất nhiều thời gian và vì vậy bạn muốn làm cho nó không đồng bộ. Đây là logic đồng bộ mà bạn bắt đầu:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem trả về một Tác vụ, cuối cùng sẽ tạo ra một số kết quả mà bạn muốn kiểm tra. Nếu kết quả hiện tại là kết quả bạn đang tìm kiếm, bạn cập nhật giá trị của một số bộ đếm trên giao diện người dùng và trả về từ phương thức. Nếu không, bạn tiếp tục xử lý các mục khác từ LoadNextItem.
Ý tưởng đầu tiên cho phiên bản không đồng bộ: chỉ cần sử dụng liên tục! Và chúng ta hãy bỏ qua phần lặp lại trong lúc này. Ý tôi là, điều gì có thể xảy ra?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Tuyệt vời, bây giờ chúng ta có một phương pháp không chặn! Thay vào đó, nó bị treo. Mọi cập nhật đối với các điều khiển giao diện người dùng sẽ xảy ra trên chuỗi giao diện người dùng, vì vậy bạn sẽ cần tính đến điều đó. Rất may, có một tùy chọn để chỉ định cách lập lịch trình liên tục và có một tùy chọn mặc định cho điều này:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Tuyệt vời, bây giờ chúng tôi có một phương pháp không sụp đổ! Thay vào đó, nó thất bại một cách âm thầm. Bản thân các nhiệm vụ tiếp tục là các nhiệm vụ riêng biệt, với trạng thái của chúng không gắn với trạng thái của nhiệm vụ trước đó. Vì vậy, ngay cả khi LoadNextItem lỗi, người gọi sẽ chỉ thấy một tác vụ đã hoàn thành thành công. Được rồi, chỉ cần chuyển ngoại lệ, nếu có:
return LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
throw t.Exception.InnerException;
}
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Tuyệt vời, bây giờ điều này thực sự hoạt động. Đối với một mặt hàng duy nhất. Bây giờ, làm thế nào về vòng lặp đó. Hóa ra, một giải pháp tương đương với logic của phiên bản đồng bộ ban đầu sẽ trông giống như sau:
Task AsyncLoop() {
return AsyncLoopTask().ContinueWith(t =>
Counter.Value = t.Result,
TaskScheduler.FromCurrentSynchronizationContext());
}
Task<int> AsyncLoopTask() {
var tcs = new TaskCompletionSource<int>();
DoIteration(tcs);
return tcs.Task;
}
void DoIteration(TaskCompletionSource<int> tcs) {
LoadNextItem().ContinueWith(t => {
if (t.Exception != null) {
tcs.TrySetException(t.Exception.InnerException);
} else if (t.Result.Contains("target")) {
tcs.TrySetResult(t.Result.Length);
} else {
DoIteration(tcs);
}});
}
Hoặc, thay vì tất cả những điều trên, bạn có thể sử dụng async để làm điều tương tự:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Bây giờ đẹp hơn rất nhiều, phải không?
Wait
cuộc gọi trong ví dụ thứ hai thì hai đoạn mã sẽ (hầu hết) tương đương.