异步操作已取消,但仍需要时间来更新网格

时间:2019-01-14 20:25:55

标签: c# async-await

我在使异步操作正常工作(异步新手)时遇到了一些麻烦。我的目标是让按钮“加载数据”熄灭并从数据库中检索一些数据并填充网格。对于某些用户而言,数据库可能会有点遥远,此操作可能需要一些时间。考虑到这一点,我希望用户能够选择取消并选择检索较小的数据集。

我主要使用当前流程:

  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 :(得分:1)

这是将您的代码修改为C#惯用的async

请注意以下几点:

  • 异步代码通常是指涉及异步IO的操作,其中完成信号(和后续的完成回调)基本上是由硬件中断和操作系统发出的-即使代码在另一个操作系统上运行,也不应将其与并发(即多线程)相混淆。线程在概念上也可以建模为Task(实际上,Task用于多线程(Task.Run)和async-IO。
    • 无论如何,关键是:如果您使用的是async-IO API(例如SqlDataReaderFileStreamNetworkStream等),那么您可能不需要不想使用Task.Run
  • 必须在UI线程中运行的代码(例如WinForms和WPF UI代码)you should always use .ConfigureAwait(false)之外,以允许在可用的后台线程中调用完成回调,这意味着不会强制执行UI线程运行后台代码。
  • 通常来说,切勿使用Task<T>.ResultTask.Wait(),因为它们会阻塞线程并带来死锁的风险(因为无法在阻塞的线程上运行继续回调)。仅在验证任务已完成后才使用Task<T>.Result(或只需执行await task)。
  • 您应该将CancellationToken传递给您调用的每个子方法Async

其他nitpicks:

  • 您可以在同一缩进级别上组合using()语句,并在创建SqlConnection.OpenAsync之后调用SqlCommand
  • 参数应为camelCase而不是PascalCase
  • 引用实例成员(字段,方法,属性等)的前缀应为this.,以便在视觉上与本地标识符区分开。
  • 执行if( this.x != null ) this.x.Foo()并不完全安全,因为在多线程程序中x可以用{em> if和{{ 1}}呼叫。而是使用.Foo()运算符,该运算符保留本地引用,以防止地毯从您的下方拉出(它的工作方式为:?.,保证了线程安全)。
  • X lx = this.x; if( lx != null ) lx.Foo()是(可能是)UI组件,不应从概念上“后台”的函数(如BindingList方法中返回),因此我先返回FindForLocationAsync然后返回UI将List<T>包裹在List<T>中。

代码:

BindingList<T>