Асинхронная операция отменена, но все еще требуется время для обновления сетки - PullRequest
0 голосов
/ 14 января 2019

У меня возникли некоторые проблемы с работой асинхронной операции (новинка для асинхронной работы). Моя цель состоит в том, чтобы кнопка «Загрузить данные» вышла и извлекла некоторые данные из базы данных и заполнила сетку. Для некоторых пользователей база данных может быть довольно далеко, и эта операция может занять некоторое время. Учитывая это, я хочу, чтобы пользователи имели возможность отменить и выбрать получение меньшего набора данных.

У меня он в основном работает с током:

  1. Пользователь нажимает кнопку «Загрузить данные ...»
  2. Кнопка меняется на «Отмена» и Асинхронная операция для извлечения данных начинается
  3. Данные получены и сетка заполнена

Это все работает хорошо, ЗА ИСКЛЮЧЕНИЕМ, если пользователь нажимает кнопку «Отмена», все равно требуется столько же времени, сколько потребовалось бы для того, чтобы все данные для сетки оказались пустыми. Это заставляет меня поверить, что длительная операция на самом деле не была отменена ... однако, когда я отлаживаю в методе «FindForLocationAsync», токен отмены действительно останавливает итеративную операцию и рано возвращается из метода, если пользователь запрашивает отмена.

Я читал об этом столько времени, сколько мог, но сейчас я в некотором тупике. Любая помощь будет принята с благодарностью.

enter image description here

Источник токена отмены

CancellationTokenSource cancellationTokenSource = null;

Метод нажатия кнопки

private async void btnSearch_Click(object sender, EventArgs e)
{
    gridLog.DataSource = null;
    Cursor = Cursors.WaitCursor;

    if (btnSearch.Text.ToLower().Contains("load"))
    {
        btnSearch.Text = "Cancel";
        btnSearch.ForeColor = Color.White;
        btnSearch.BackColor = Color.Red;

        //get params to pass
        /* snip */

        cancellationTokenSource = new CancellationTokenSource();
        await Task.Run(() =>
            {
                var ds = DocLog.FindForLocationAsync(docType, subType, days, currLocation.ID, cancellationTokenSource.Token).Result;
                gridLog.DataSource = ds;
            });

        btnSearch.Text = "Load Data...";
        btnSearch.ForeColor = Color.Black;
        btnSearch.BackColor = Color.FromArgb(225, 225, 225);
    }
    else
    {
        cancelSearch();
        btnSearch.Text = "Load Data...";
        btnSearch.ForeColor = Color.Black;
        btnSearch.BackColor = Color.FromArgb(225, 225, 225);
    }

    Cursor = Cursors.Default;
}

Способ отмены

private void cancelSearch()
{
    if (cancellationTokenSource != null) cancellationTokenSource.Cancel();
}

Метод длительного бега

public async static Task<BindingList<DocLog>> FindForLocationAsync(string DocType, string SubType, int? LastXDays, Guid LocationID, CancellationToken CancellationToken)
{
    BindingList<DocLog> dll = new BindingList<DocLog>();

    using (SqlConnection sqlConnection = new SqlConnection(Helper.GetConnectionString()))
    {
        sqlConnection.Open();
        using (SqlCommand sqlCommand = new SqlCommand((LastXDays == null) ? "DocLogGetAllForLocation" : "DocLogGetAllForLocationLastXDays", sqlConnection))
        {
            sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
            sqlCommand.Parameters.Add("@DocType", SqlDbType.NVarChar, 30).Value = DocType.Trim();
            sqlCommand.Parameters.Add("@SubType", SqlDbType.NVarChar, 30).Value = SubType.Trim();
            sqlCommand.Parameters.Add("@LocationID", SqlDbType.UniqueIdentifier).Value = LocationID;
            if (LastXDays != null) { sqlCommand.Parameters.Add("@NumberOfDays", SqlDbType.Int).Value = LastXDays; }

            SqlDataReader sqlDataReader = sqlCommand.ExecuteReader();

            await Task.Run(() =>
            {
                while (sqlDataReader.Read())
                {
                    if (CancellationToken.IsCancellationRequested)
                    {
                        dll = new BindingList<DocLog>();
                        break;
                    }
                    else
                    {
                        DocLog dl = readData(sqlDataReader);
                        dll.Add(dl);
                    }
                }
            });
        }
    }

    return dll;
}

1 Ответ

0 голосов
/ 15 января 2019

Вот ваш код, модифицированный как C # идиоматический async:

Обратите внимание на следующее:

  • Асинхронный код, как правило, относится к операциям, включающим асинхронный ввод-вывод, когда сигнал завершения (и последующий обратный вызов завершения) в основном создается аппаратным прерыванием и ОС - его не следует путать с параллелизмом (то есть многопоточностью), даже если код выполняется на другом Поток также может быть концептуально смоделирован как Task (действительно, Task используется как для многопоточности (Task.Run), так и для асинхронного ввода-вывода).
    • В любом случае, смысл в том, что если вы используете async -IO API (например, SqlDataReader, FileStream, NetworkStream и т. Д.), То вы, вероятно, не хотите использовать Task.Run .
  • За пределами кода, который должен выполняться в потоке пользовательского интерфейса (т. Е. WinForms и код пользовательского интерфейса WPF) вы всегда должны использовать .ConfigureAwait(false), чтобы разрешить обратный вызов завершения в доступном фоновом потоке, что означает поток пользовательского интерфейса не будет вынужден запускать фоновый код.
  • Вообще говоря, никогда не используйте Task<T>.Result или Task.Wait(), поскольку они блокируют поток и создают риск взаимоблокировки (потому что обратный вызов продолжения не может быть запущен в заблокированном потоке). Используйте Task<T>.Result только после того, как вы проверите задание (или просто await task).
  • Вы должны передать CancellationToken каждому дочернему Async методу, который вы вызываете.

Другие клюшки:

  • Вы можете объединить операторы using() на одном уровне отступа и вызывать SqlConnection.OpenAsync после того, как вы создали SqlCommand.
  • Параметры должны быть camelCase, а не PascalCase.
  • Ссылки на члены экземпляра (поля, методы, свойства и т. Д.) Должны начинаться с префикса this., чтобы они визуально отличались от локальных идентификаторов.
  • Выполнение if( this.x != null ) this.x.Foo() не совсем безопасно, поскольку в многопоточной программе x может быть заменено другим значением между , if и .Foo(). Вместо этого используйте оператор ?., который хранит локальную справку, чтобы не вытащить ковер из-под вас (он работает так: X lx = this.x; if( lx != null ) lx.Foo(), который гарантированно безопасен для нитей).
  • BindingList является (возможно) компонентом пользовательского интерфейса и не должен возвращаться из концептуально «фоновой» функции, такой как ваш метод FindForLocationAsync, поэтому я возвращаю List<T> вместо этого, а затем пользовательский интерфейс переносит List<T> в BindingList<T>.

Код:

private async void btnSearch_Click(object sender, EventArgs e)
{
    this.gridLog.DataSource = null;
    this.Cursor = Cursors.WaitCursor;

    if (this.btnSearch.Text.ToLower().Contains("load"))
    {
        this.btnSearch.Text = "Cancel";
        this.btnSearch.ForeColor = Color.White;
        this.btnSearch.BackColor = Color.Red;

        //get params to pass
        /* snip */

        this.cancellationTokenSource = new CancellationTokenSource();

        List<DocLog> list = await DocLog.FindForLocationAsync(docType, subType, days, currLocation.ID, cancellationTokenSource.Token);
        gridLog.DataSource = new BindingList<DocLog>( list );

        this.btnSearch.Text = "Load Data...";
        this.btnSearch.ForeColor = Color.Black;
        this.btnSearch.BackColor = Color.FromArgb(225, 225, 225);
    }
    else
    {
        CancelSearch();
        this.btnSearch.Text = "Load Data...";
        this.btnSearch.ForeColor = Color.Black;
        this.btnSearch.BackColor = Color.FromArgb(225, 225, 225);
    }

    this.Cursor = Cursors.Default;
}

private void CancelSearch()
{
    this.cancellationTokenSource?.Cancel();
}

public async static Task<List<DocLog>> FindForLocationAsync(string DocType, string SubType, int? LastXDays, Guid LocationID, CancellationToken cancellationToken)
{
    List<DocLog> dll = new List<DocLog>();

    using (SqlConnection sqlConnection = new SqlConnection(Helper.GetConnectionString()))
    using (SqlCommand sqlCommand = sqlConnection.CreateCommand())
    {
        await sqlConnection.OpenAsync(cancellationToken).ConfigureAwait(false);

        sqlCommand.CommandText = (LastXDays == null) ? "DocLogGetAllForLocation" : "DocLogGetAllForLocationLastXDays";
        sqlCommand.CommandType = System.Data.CommandType.StoredProcedure;
        sqlCommand.Parameters.Add("@DocType", SqlDbType.NVarChar, 30).Value = DocType.Trim();
        sqlCommand.Parameters.Add("@SubType", SqlDbType.NVarChar, 30).Value = SubType.Trim();
        sqlCommand.Parameters.Add("@LocationID", SqlDbType.UniqueIdentifier).Value = LocationID;
        if (LastXDays != null) { sqlCommand.Parameters.Add("@NumberOfDays", SqlDbType.Int).Value = LastXDays; }

        using( SqlDataReader sqlDataReader = await sqlCommand.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false) )
        {
            while (await sqlDataReader.ReadAsync(cancellationToken).ConfigureAwait(false))
            {
                if (cancellationToken.IsCancellationRequested) break;

                DocLog dl = readData(sqlDataReader);
                dll.Add(dl);
            }
        }
    }

    return dll;
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...