Пользователь только перешел на страницу и тут же ушел с неё, не дожидаясь завершения загрузки. Если это была простая html страница, то ничего ужасного не произошло. Но что если страница делает несколько "тяжелых" запросов?
Конечно, ошибки в этом случае не будет, но зачем загружать сервер бесполезной работой? На помощь приходит ASP.NET и его стандартные CancellationToken.
CancellationToken
CancellationToken это структура, которая предназначена для информирования о необходимости немедленной отмены текущей операции. В контексте данной статьи не будем рассматривать детали её внутренней реализации. Просто представим её как флаг, сигнализирующий об отмене операции.
Стандартные CancellationToken в ASP.NET MVC (доступны начиная с .NET 4.5)
ClientDisconnectedToken
Уведомляет об отключении клиента.
- ASP.NET: свойство ClientDisconnectedToken класса HttpResponseBase / HttpResponse;
- ASP.NET MVC: свойство Контроллера this.Response.ClientDisconnectedToken.
TimedOutToken
Сигнализирует о таймауте запроса.
- ASP.NET: свойство TimedOutToken класса HttpRequestBase / HttpRequest;
- ASP.NET MVC: свойство Контроллера this.Request.TimedOutToken.
Объединяем CancellationToken
Для создания CancellationToken в .NET существует класс CancellationTokenSource. Он также позволяет объединять несколько CancellationToken в один по принципу "или". Пример:
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
this.Response.ClientDisconnectedToken, this.Request.TimedOutToken);
CancellationToken token = tokenSource.Token;
Примеры
Асинхронные Действия в ASP.NET MVC
Многие асинхронные операции принимают CancellationToken в качестве параметра. При его срабатывании сам вызов заканчивается выбросом исключения TaskCanceledException.
Перейдём сразу к примеру. "Тяжелый" асинхронный сервис будем имитировать с помощью Task.Delay(). Для получения информации о происходящем воспользуемся Debug.WriteLine().
Создадим следующее действие в Контроллере. Содержимое Представления Index может быть любым.
public async Task<ActionResult> Index()
{
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
this.Response.ClientDisconnectedToken, this.Request.TimedOutToken);
var sw = new Stopwatch();
sw.Restart();
try {
await Task.Delay(4000, tokenSource.Token);
Debug.WriteLine("First call time: {0} ms", sw.ElapsedMilliseconds);
await Task.Delay(6000, tokenSource.Token);
Debug.WriteLine("Total time: {0} ms", sw.ElapsedMilliseconds);
}
catch (TaskCanceledException) {
Debug.WriteLine("Task cancelled. Total time: {0} ms", sw.ElapsedMilliseconds);
}
sw.Stop();
return this.View();
}
Теперь можно запустить проект и посмотреть на результаты в окне Output. После успешной полной загрузке страницы там будет сообщения, указывающее на общее время, равное примерно 10 секундам.
First call time: 4002 ms
Total time: 10015 ms
Откройте дополнительную вкладку в Internet Explorer, чтобы сессия отладки не закрылась в следующем эксперименте. Теперь вернитесь, обновите страницу сайта и тут же закройте её. В Output (Debug) будет что-то вроде следующего:
A first chance exception of type 'TaskCanceledException' occurred in mscorlib.dll
Task cancelled. Total time: 782 ms
The program 'iexplore.exe' has exited with code 0 (0x0).
Как хорошо видно, сервер прекратил обработку после закрытия страницы пользователем. Требуемое поведение достигнуто.
Для полноты эксперимента не будем передавать tokenSource.Token в вызовы "сервиса". В этом случае, даже при закрытии страницы оба вызова будут полностью отработаны.
First call time: 4005 ms
Total time: 10010 ms
Синхронные операции в ASP.NET
Но что делать если методы, которые вызывает Контроллер, синхронные? CancellationToken это обычная структура и ничего не мешает использовать её и в этом случае. Вот пример, в котором тяжелые вызовы имитируются с помощью Thread.Sleep():
public ActionResult SyncIndex()
{
var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
this.Response.ClientDisconnectedToken, this.Request.TimedOutToken);
var sw = new Stopwatch();
sw.Restart();
for (int i = 0; i < 10; i++) {
Thread.Sleep(1000);
if (tokenSource.Token.IsCancellationRequested) {
Debug.WriteLine("SyncTest> Cancellation requested.");
break;
}
}
sw.Stop();
Debug.WriteLine("SyncTest> Total time: {0} ms", sw.ElapsedMilliseconds);
return this.View("Index");
}
Разумеется, прервать сам вызов сервиса в данном случае не возможно. Хотя можно предусмотреть и это, если доступен его исходный код. Но при любом случае, часть ненужной работы можно легко отменить.
В завершении стоит отметить, что использовать приведенные способы следует с оглядкой на логику приложения. Например, если идет сохранение введенных данных, то их можно отметить как "черновик".