Andrey on .NET | ASP.NET CancellationToken

ASP.NET CancellationToken

Пользователь только перешел на страницу и тут же ушел с неё, не дожидаясь завершения загрузки. Если это была простая 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");
}

Разумеется, прервать сам вызов сервиса в данном случае не возможно. Хотя можно предусмотреть и это, если доступен его исходный код. Но при любом случае, часть ненужной работы можно легко отменить.

В завершении стоит отметить, что использовать приведенные способы следует с оглядкой на логику приложения. Например, если идет сохранение введенных данных, то их можно отметить как "черновик".

Комментарии (3) -

Есть проект  на ASP .NET 4 (.NET 4).

Попробовал:

var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(
        this.Response.ClientDisconnectedToken, this.Request.TimedOutToken);

1) Оказалось в Response нет таких свойств для передачи их в качестве параметра

2) В методе предлагается использовать перечисление CancellationToken. В перечислении CancellationToken, есть только None

Вот обида, может подскажешь, как отследить уход со странички? а то у меня в контроллере бесконечныфй цикл, его надо как-то завершить при уходе со страницы... проект на net 4

Добавить комментарий