Ось послідовність фрагментів коду, яку я нещодавно використовував, щоб проілюструвати різницю та різні проблеми за допомогою асинхронних рішень.
Припустимо, у вашому додатку на основі GUI є якийсь обробник подій, який займає багато часу, і тому ви хочете зробити його асинхронним. Ось синхронна логіка, з якої ви починаєте:
while (true) {
string result = LoadNextItem().Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
LoadNextItem повертає завдання, яке в підсумку призведе до певного результату, який ви хочете перевірити. Якщо поточний результат такий, який ви шукаєте, ви оновлюєте значення деякого лічильника в інтерфейсі і повертаєтесь з методу. В іншому випадку ви продовжуєте обробляти більше елементів з LoadNextItem.
Перша ідея для асинхронної версії: просто використовуйте продовження! І давайте на даний момент ігноруємо циклічну частину. Я маю на увазі, що може піти не так?
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
});
Чудово, зараз у нас є метод, який не блокує! Він замість цього виходить з ладу. Будь-які оновлення елементів управління користувальницьким інтерфейсом повинні відбуватися в потоці користувальницького інтерфейсу, тому вам потрібно буде це врахувати. На щастя, є можливість вказати, як слід планувати продовження, і для цього є лише стандартний:
return LoadNextItem().ContinueWith(t => {
string result = t.Result;
if (result.Contains("target")) {
Counter.Value = result.Length;
}
},
TaskScheduler.FromCurrentSynchronizationContext());
Чудово, зараз у нас є метод, який не виходить з ладу! Замість цього він виходить з ладу. Продовження є окремими завданнями, їх статус не пов'язаний з попереднім завданням. Тож навіть якщо LoadNextItem помилки, абонент побачить лише завдання, яке було успішно виконано. Добре, тоді просто передайте виняток, якщо такий є:
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());
Чудово, зараз це насправді працює. Для одного предмета. Тепер, як щодо цього циклу. Виявляється, рішення, еквівалентне логіці оригінальної синхронної версії, буде виглядати приблизно так:
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);
}});
}
Або замість усього перерахованого вище ви можете використовувати async, щоб зробити те саме:
async Task AsyncLoop() {
while (true) {
string result = await LoadNextItem();
if (result.Contains("target")) {
Counter.Value = result.Length;
break;
}
}
}
Зараз це набагато приємніше, чи не так?
Wait
виклик у другому прикладі , то два сниппет будуть ( в основному) еквівалентні.