Вот последовательность фрагментов кода, которые я недавно использовал, чтобы проиллюстрировать разницу и различные проблемы с использованием асинхронного решения.
Предположим, у вас есть обработчик событий в вашем приложении на основе графического интерфейса пользователя, который требует много времени, и поэтому вы хотите сделать его асинхронным. Вот синхронная логика, с которой вы начинаете:
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
вызов во втором примере , то два сниппет будут ( в основном) эквивалентны.