Отмена задачи с помощью асинхронной задачи

Я пытаюсь использовать токены отмены, как описано в это часто задаваемые вопросы. Это была моя первоначальная мысль:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
    if (this.cancelToken == null)
    {
        this.cancelToken = new CancellationTokenSource();
    }

    try
    {
        bool loginSuccess = await AsyncLoginTask(this.cancelToken.Token);

        if (loginSuccess)
        {
            // Show main page
        }
    }
    catch (OperationCanceledException ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
    finally
    {
        this.cancelToken = null;
    }
}

private async Task<bool> AsyncLoginTask(CancellationToken cancellationToken = default(CancellationToken))
{
    // Pass the token to HttpClient()
}

Сейчас я его адаптировал и вот результат:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
    this.cancelToken?.Dispose();
    this.cancelToken = new CancellationTokenSource();

    try
    {
        var ui = TaskScheduler.FromCurrentSynchronizationContext();
        var loginTask = Task.Factory.StartNew(async () =>
        {
            bool loginSuccess = await AsyncLoginTask(this.cancelToken.Token);
        }, this.cancelToken.Token);

        var displayResults = loginTask.ContinueWith(resultTask =>
                            {
                                // How do I know if the login was successful?
                                // Because AsyncLoginTask() returns bool.
                                System.Diagnostics.Debug.WriteLine("done");
                            },
                             CancellationToken.None,
                             TaskContinuationOptions.OnlyOnRanToCompletion,
                             ui);

        var displayCancelledTasks = loginTask.ContinueWith(resultTask =>
                                    {
                                        System.Diagnostics.Debug.WriteLine("canceled");
                                    },
                                   CancellationToken.None,
                                   TaskContinuationOptions.OnlyOnCanceled, ui);
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
    }
}

Вопросы:

  • Как я узнаю, что вход был успешным? Поскольку AsyncLoginTask() возвращает bool.
  • Как правильно создать и уничтожить токен, чтобы разрешить запуск и отмену операции несколько раз?
  • Как обрабатывать задачу в задаче? «Готово» отображается в консоли, а задача (AsyncLoginTask) еще не завершена.

person testing    schedule 30.05.2017    source источник
comment
How do I know if the login was successful? Because AsyncLoginTask() returns bool ну можно вернуть loginSuccess в той задаче, что даст loginTask результат. Но... мой вопрос заключается в том, почему вы хотите обернуть задачу входа в задачу?   -  person Stefan    schedule 30.05.2017
comment
Мой старый код (AsyncLoginTask) был построен таким образом (асинхронно/ожидание с возвратом true/false). Теперь я хочу, чтобы пользователь мог отменить операцию. В связанной статье это рекомендуемый способ работы с задачей и ContinueWith. Итак, я завершил свою задачу. Должен ли я остаться с ловлей OperationCanceledException?   -  person testing    schedule 30.05.2017
comment
Что ж, я не углублялся в связанную статью, и я должен сказать, что я не эксперт в этой области... Но, поскольку при входе в систему уже есть возможность предоставить токен отмены, и ваша логика позже потребует результат входа в систему, простой await кажется мне более интуитивным. Вы можете справиться с этой отменой соответствующим образом. Можете ли вы объяснить свою настоящую цель и почему вы думаете, что этот рефакторинг поможет ее достичь?   -  person Stefan    schedule 30.05.2017
comment
Возможно, вам лучше посмотреть Microsoft Docs для асинхронного программирования. Task.Factory.StartNew и .ContinueWith устарели и заменены шаблоном async await   -  person Default    schedule 30.05.2017
comment
@testing, посмотрите этот пост в блоге: Асинхронный повторный вход и шаблоны для работы с ним.   -  person noseratio    schedule 30.05.2017
comment
@Stefan: Фактическая цель состоит в том, чтобы включить отмену (например, пользователь может отменить операцию или, если пользователь уходит, чтобы минимизировать странное поведение, отсутствие дублированных вызовов, меньше исключений нулевых ссылок и т. д.). Я хотел использовать этот метод из-за утверждения Теперь я получаю те же результаты, что и с блоком try-catch, и мне вообще не нужно иметь дело с исключениями. Подчеркну, что это более естественный способ работы с TPL и задачами. Но я думаю, что останусь со своим старым подходом, за исключением того, что у одного есть лучший метод передовой практики.   -  person testing    schedule 30.05.2017
comment
Что не так с вашей первой версией? StartNew и ContinueWith в некоторых случаях могут привести к неожиданным результатам. И done пишется в консоли, потому что await во внутренней задаче завершается, пока вы не получите результат из http-запроса.   -  person VMAtm    schedule 30.05.2017
comment
@VMAtm: я раньше не работал с CancellationTokenSource и пробовал кое-что, не зная, правильно ли это. Потом нашел статью, которая привела ко второй версии. Кроме того, когда я должен поймать TaskCanceledException, когда OperationCanceledException? Исключение, которое я получил, было своего рода ограниченным (я действительно не мог отлаживать), но, похоже, это было OperationCanceledException. Я знаю, что внутренняя задача завершается, пока я не получу результат, но я не понимаю, почему. Поскольку я откажусь от второго подхода, этот вопрос сейчас менее актуален.   -  person testing    schedule 30.05.2017
comment
Внутренняя задача завершается, потому что так работает await — она возвращается из первого await, сохраняет контекст и освобождает поток. Вы должны поймать OperationCanceledException, это базовый класс, stackoverflow.com/a/13040503/213550   -  person VMAtm    schedule 30.05.2017


Ответы (1)


Я пытаюсь использовать токены отмены, как описано в этом FAQ.

В этом сообщении в блоге используется динамический параллелизм задач (StartNew и ContinueWith). Динамический параллелизм задач — это когда у вас есть много операций, связанных с процессором, и вы не знаете, сколько у вас их, пока вы их уже не обработаете (т. е. каждая обрабатываемая вами операция может добавить ноль или более дополнительных задач к той же процесс).

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

Вы бы хотели сделать это примерно так:

private async void OnLoginButtonClicked(object sender, EventArgs e)
{
  // Cancel the previous attempt (if any) and start a new one.
  this.cts?.Cancel();
  this.cts = new CancellationTokenSource();

  try
  {
    bool loginSuccess = await AsyncLoginTask(this.cts.Token);
    // Resolve race condition where user cancels just as it completed.
    this.cts.Token.ThrowIfCancellationRequested();
    if (loginSuccess)
    {
      // Show main page
    }
  }
  catch (OperationCanceledException ex)
  {
    System.Diagnostics.Debug.WriteLine(ex.Message);
  }
  catch (Exception ex)
  {
    System.Diagnostics.Debug.WriteLine(ex.Message);
  }
}

private async Task<bool> AsyncLoginTask(CancellationToken cancellationToken = default(CancellationToken))
{
  // Pass the token to HttpClient()
}
person Stephen Cleary    schedule 30.05.2017
comment
Спасибо за Ваш ответ. Если кнопка входа нажата еще раз, токен отмены должен быть создан заново? - person testing; 30.05.2017
comment
Да потому что любой уже существующий CTS для более раннего клика. - person Stephen Cleary; 30.05.2017