使用ExecuteNonQueryAsync并报告进度

8
我原以为我在做一件非常简单的事情。我只想在屏幕上报告一个运行中的数字,以便用户了解我正在执行的SQL存储过程正在工作,并且他们不会不耐烦地开始点击按钮。
问题是我无法弄清楚如何实际调用ExecutNonQueryAsync命令的进度报告器。它会卡在我的报告循环中,从未执行该命令,但如果我将其放在异步命令之后,它将被执行,结果永远不会等于零。
如果您有任何想法、评论或建议,将不胜感激。非常感谢!
        int i = 0;
        lblProcessing.Text = "Transactions " + i.ToString();
        int result = 0;
        while (result==0)
        {
            i++;
            if (i % 500 == 0)
            {
                lblProcessing.Text = "Transactions " + i.ToString();
                lblProcessing.Refresh();
            }

        }
        //  Yes - I know - the code never gets here - that is the problem! 
        result = await cmd.ExecuteNonQueryAsync();

据我所知,目前没有任何方法可以让SQL Server向您报告进度。 - Bradley Uffner
我看到了。我原本希望用ExecuteNonQueryAsync()方法来实现,不过还是谢谢你 :) - Missy
1
你试过了吗?快速浏览让我认为它会工作。我没有看到任何异步阻塞问题会阻止它。 - Bradley Uffner
我会再仔细看看,看看能否找到让它正常工作的方法。 - Missy
只是一个建议,你可以删除SQL标签,因为这个问题并不涉及任何SQL问题或回答,只是我的建议。 - Mr.J
显示剩余3条评论
5个回答

5

最简单的方法是使用第二个连接来监视进度并报告。以下是一个小样例,可帮助您开始:

using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Text;
using System.Threading.Tasks;

namespace Microsoft.Samples.SqlServer
{
    public class SessionStats
    {
        public long Reads { get; set; }
        public long Writes { get; set; }
        public long CpuTime { get; set; }
        public long RowCount { get; set; }
        public long WaitTime { get; set; }
        public string LastWaitType { get; set; }
        public string Status { get; set; }

        public override string ToString()
        {
            return $"Reads {Reads}, Writes {Writes}, CPU {CpuTime}, RowCount {RowCount}, WaitTime {WaitTime}, LastWaitType {LastWaitType}, Status {Status}";
        }
    }
    public class SqlCommandWithProgress
    {


        public static async Task ExecuteNonQuery(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            using (var rdr = await ExecuteReader(ConnectionString, Query, OnProgress))
            {
                rdr.Dispose();
            }
        }

        public static async Task<DataTable> ExecuteDataTable(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            using (var rdr = await ExecuteReader(ConnectionString, Query, OnProgress))
            {
                var dt = new DataTable();

                dt.Load(rdr);
                return dt;
            }
        }


        public static async Task<SqlDataReader> ExecuteReader(string ConnectionString, string Query, Action<SessionStats> OnProgress)
        {
            var mainCon = new SqlConnection(ConnectionString);
            using (var monitorCon = new SqlConnection(ConnectionString))
            {
                mainCon.Open();
                monitorCon.Open();



                var cmd = new SqlCommand("select @@spid session_id", mainCon);
                var spid = Convert.ToInt32(cmd.ExecuteScalar());

                cmd = new SqlCommand(Query, mainCon);

                var monitorQuery = @"
select s.reads, s.writes, r.cpu_time, s.row_count, r.wait_time, r.last_wait_type, r.status
from sys.dm_exec_requests r
join sys.dm_exec_sessions s 
  on r.session_id = s.session_id
where r.session_id = @session_id";

                var monitorCmd = new SqlCommand(monitorQuery, monitorCon);
                monitorCmd.Parameters.Add(new SqlParameter("@session_id", spid));

                var queryTask = cmd.ExecuteReaderAsync( CommandBehavior.CloseConnection );

                var cols = new { reads = 0, writes = 1, cpu_time =2,row_count = 3, wait_time = 4, last_wait_type = 5, status = 6 };
                while (!queryTask.IsCompleted)
                {
                    var firstTask = await Task.WhenAny(queryTask, Task.Delay(1000));
                    if (firstTask == queryTask)
                    {
                        break;
                    }
                    using (var rdr = await monitorCmd.ExecuteReaderAsync())
                    {
                        await rdr.ReadAsync();
                        var result = new SessionStats()
                        {
                            Reads = Convert.ToInt64(rdr[cols.reads]),
                            Writes = Convert.ToInt64(rdr[cols.writes]),
                            RowCount = Convert.ToInt64(rdr[cols.row_count]),
                            CpuTime = Convert.ToInt64(rdr[cols.cpu_time]),
                            WaitTime = Convert.ToInt64(rdr[cols.wait_time]),
                            LastWaitType = Convert.ToString(rdr[cols.last_wait_type]),
                            Status = Convert.ToString(rdr[cols.status]),
                        };
                        OnProgress(result);

                    }

                }
                return queryTask.Result;


            }
        }
    }
}

你会称之为这样:

    class Program
    {

        static void Main(string[] args)
        {
            Run().Wait();

        }
        static async Task Run()
        {
            var constr = "server=localhost;database=tempdb;integrated security=true";
            var sql = @"
set nocount on;
select newid() d
into #foo
from sys.objects, sys.objects o2, sys.columns 
order by newid();
select count(*) from #foo;
";

            using (var rdr = await SqlCommandWithProgress.ExecuteReader(constr, sql, s => Console.WriteLine(s)))
            {
                if (!rdr.IsClosed)
                {
                    while (rdr.Read())
                    {
                        Console.WriteLine("Row read");
                    }
                }
            }
            Console.WriteLine("Hit any key to exit.");
            Console.ReadKey();


        }
    }

这将输出:

Reads 0, Writes 0, CPU 1061, RowCount 0, WaitTime 0, LastWaitType SOS_SCHEDULER_YIELD, Status running
Reads 0, Writes 0, CPU 2096, RowCount 0, WaitTime 0, LastWaitType SOS_SCHEDULER_YIELD, Status running
Reads 0, Writes 0, CPU 4553, RowCount 11043136, WaitTime 198, LastWaitType CXPACKET, Status suspended
Row read
Hit any key to exit.

这是一个非常有趣的想法。我在想它是否会大幅增加处理时间。 - Missy
1
它不应明显增加处理时间。 - David Browne - Microsoft
非常感谢您长篇思考后的回答。我正在努力完全理解它。在 await SqlCommandWithProgress.ExecuteReader(constr, sql, s => Console.WriteLine(s))) 中,s => Console.WriteLine(s) 是否像一个报告进度的函数?如果是,S 最初是从哪里派生的? - Missy
1
该方法接受一个类型为Action<SessionStats>的参数,它是您提供的函数或代码片段,它接受一个SessionStats实例并对其执行某些操作。在这种情况下,只需将其写入控制台。 - David Browne - Microsoft

4

在这里,你不能指望ExecuteNonQueryAsync能够实现你想要的功能。如果要实现你所需的功能,方法的结果必须是逐行或增量式地以块为单位返回SQL调用结果,但这不是提交查询批处理到SQL Server的工作方式,也不是从开销角度来看你希望它工作的方式。你将一个SQL语句交给服务器,在它完成处理该语句后,它会返回该语句所影响的总行数。


我在想,如果不是为了释放UI线程,他们为什么会提供Async功能。 - Missy
1
@Missy 这并不是为了释放 UI 线程,而是为了释放 ASP.NET 线程池中的线程,以便处理更多的用户请求。 - Chamika Goonetilaka

3

您是否只需要让用户知道正在发生某些事情,而实际上不需要显示当前进度?

如果是这样的话,您可以只显示一个ProgressBar,并将其Style设置为Marquee

如果您希望这是一个“自包含”的方法,您可以在模态窗体上显示进度条,并将该窗体代码包含在方法本身中。

例如:

public void ExecuteNonQueryWithProgress(SqlCommand cmd) {
    Form f = new Form() {
        Text = "Please wait...",
        Size = new Size(400, 100),
        StartPosition = FormStartPosition.CenterScreen,
        FormBorderStyle = FormBorderStyle.FixedDialog,
        MaximizeBox = false,
        ControlBox = false
    };
    f.Controls.Add(new ProgressBar() { 
        Style = ProgressBarStyle.Marquee,
        Dock = DockStyle.Fill
    });
    f.Shown += async (sender, e) => {
        await cmd.ExecuteNonQueryAsync();
        f.Close();
    };
    f.ShowDialog();
}

我也想到了这一点,但我希望能够得到一段简洁的代码,以便在实用程序中使用和重复利用。进度条需要调用程序具有进度条显示功能。 - Missy
我明白了,但是你当前的代码需要访问一个标签,对吗?没什么不同。 - Blorgbeard
收到。我会考虑这个建议,但我希望能找到更完整的解决方案。 - Missy
一种实现“捆绑”解决方案的方法(假设您仍然坚持winforms世界)是弹出一个带有进度条的模态对话框。该表单足够简单,可以在运行时在您的方法内创建:您不需要使用设计器来创建它。 - Blorgbeard
这是最直接的答案。您能否看一下我的想法并发表评论?我也会感激您对问题的点赞以维护我的声誉。然后我会将其选为已回答。 - Missy

3

这是一个有趣的问题。我过去曾经实施过类似的事情。在我们的情况下,优先考虑:

  • 保持客户端响应,以防用户不想等待。
  • 更新用户的操作和进度。

我会使用线程在后台运行进程,例如:

HostingEnvironment.QueueBackgroundWorkItem(ct => FunctionThatCallsSQLandTakesTime(p, q, s));

然后,使用一种估算工作时间的方法,在时钟上从客户端递增进度条。为此,请查询您的数据以获取一个与 FunctionThatCallsSQLandTakesTime 所需的工作时间呈线性关系的变量。
例如; 本月活跃用户数量影响 FunctionThatCallsSQLandTakesTime 的执行时间。每10000个用户需要5分钟。因此,您可以相应地更新进度条。

有趣的解决问题方式。 - Missy

1
我在想这是否是一个合理的方法:
    IAsyncResult result = cmd2.BeginExecuteNonQuery();
    int count = 0;
    while (!result.IsCompleted)
    {
         count++;
         if (count % 500 == 0)
         {
            lblProcessing.Text = "Transactions " + i.ToString();
            lblProcessing.Refresh();
         }
         // Wait for 1/10 second, so the counter
         // does not consume all available resources 
         // on the main thread.
         System.Threading.Thread.Sleep(100);
    }

1
这不是一个坏主意,但实现存在问题。你永远不应该在UI线程上使用 Sleep。最好使用一个 Timer 并在 Tick 事件处理程序中进行处理。 - Blorgbeard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接