【问题标题】:Very slow foreach loop非常慢的foreach循环
【发布时间】:2012-08-30 17:17:00
【问题描述】:

我正在处理现有的应用程序。此应用程序从一个大文件中读取数据,然后在进行一些计算后,将数据存储在另一个表中。

但是执行此操作的循环(见下文)需要很长时间。由于文件有时包含 1,000 条记录,因此整个过程需要数天时间。

我可以用其他东西替换这个foreach 循环吗?我尝试使用Parallel.ForEach,它确实有帮助。我是新手,非常感谢您的帮助。

foreach (record someredord Somereport.r)
{
    try
    {
        using (var command = new SqlCommand("[procname]", sqlConn))
        {
            command.CommandTimeout = 0;
            command.CommandType = CommandType.StoredProcedure;
            command.Parameters.Add(…);

            IAsyncResult result = command.BeginExecuteReader();
            while (!result.IsCompleted)
            {
                System.Threading.Thread.Sleep(10);
            }
            command.EndExecuteReader(result);
        }
    }
    catch (Exception e)
    {
        …
    }
}

查看答案后,我删除了 Async 并使用如下编辑代码。但这并没有提高性能。

using (command = new SqlCommand("[sp]", sqlConn))
{
    command.CommandTimeout = 0;
    command.CommandType = CommandType.StoredProcedure;
    foreach (record someRecord in someReport.)
    {
        command.Parameters.Clear();
        command.Parameters.Add(....)
        command.Prepare();                            

        using (dr = command.ExecuteReader())
        {
            while (dr.Read())
            {
                if ()
                {

                }
                else if ()
                {

                }
            }
        }                             
    }                        
}

【问题讨论】:

  • 两个想法 - 首先,你做错了异步,结果你很可能在循环中为许多项目睡觉。第二,你能在整个循环中重用SqlCommand对象,而不是每次都创建/销毁一个吗?
  • 如果您告诉我们更多关于您想要完成的工作,我们可能会向您展示一个运行速度快几个数量级的 SQL 解决方案,并避免整个异步/并行业务完全。
  • @user1110790:您发布的代码充满了错误(并且仍然至少有一个),所以我已经清理了一下。我可以谦虚地建议,当您在 SO 上发帖时,请确保您的代码正常;否则,您可能只会让很多 cmets 专注于这一点,而不是关注实际问题。
  • 题外话: 虽然我在这里同意其他人的观点,即您以错误的方式使用异步方法,但让我补充一点,您永远不应该像这样轮询 AsyncResult那个:while (!result.IsCompleted) Thread.Sleep(…);。相反,您应该这样做:result.AsyncWaitHandle.WaitOne(); 虽然这也会阻塞调用线程,但它不需要任何轮询;操作系统将在完成后唤醒调用线程。
  • @RobertHarvey ,所以这实际上是读取包含帐户信息及其在线使用的文件的服务的一部分。根据数据,我们正在计算总使用量并使用存储过程将该信息添加到 3 个表中。存储过程只是更新现有记录或插入新记录。这些表很小,仅包含 5 列。

标签: c# .net ado.net


【解决方案1】:

与其循环sql连接这么多次,有没有考虑从sql server中提取整组数据并通过数据集处理数据?

编辑:决定进一步解释我的意思.. 您可以执行以下操作,伪代码如下

  1. 使用 select * 并从数据库中获取所有信息并将它们存储到类的 listdictionary 中。
  2. 执行您的 foreach(在 someReport 中记录 someRecord)并照常进行条件匹配。

【讨论】:

  • +1。但最好将数据加载到强类型集合中,然后在上面使用 Linq,而不是使用 DataSet。
  • 我尝试使用 datase ,但由于某种原因,它进一步减慢了进程。我们还记录每一个操作。您认为我可以在单独的线程上进行日志记录以提高性能吗?
  • @user1110790 - 根据我的经验,使用数据集通常很慢。这就是为什么我建议使用强类型集合。简单地使用内存中的 IEnumerable 集合将非常快。如果您要进行大量关键查找,使用Dictionary 可以更快。
  • @SteveWortham 这可能需要更改基类。你能给我一个例子吗?看起来这个问题没有一个简单的解决方案。非常感谢
  • @user1110790 - 我刚刚发布了一个答案,描述了我将如何做到这一点......stackoverflow.com/a/12205953/102896
【解决方案2】:

第 1 步:放弃异步尝试。它没有正确实施,无论如何你都在阻止。所以只需执行该过程,看看是否有帮助。

第 2 步: 将 SqlCommand 移出循环并在每次迭代中重复使用它。这样您就不会为循环中的每个项目产生创建和销毁它的成本。

警告:确保重置/清除/删除上一次迭代中不需要的参数。我们使用可选参数做了类似的事情,并且从上一次迭代中“流血”,因为我们没有清理我们不需要的参数!

【讨论】:

  • +1 第二步很重要,很多人忘记了。
  • “第 2 步:将 SqlCommand 移出循环...”是主要改进!!!
【解决方案3】:

你最大的问题是你在循环这个:

IAsyncResult result = command.BeginExecuteReader();

while (!result.IsCompleted)
{
   System.Threading.Thread.Sleep(10);
}

command.EndExecuteReader(result);

异步模型的整个想法是调用线程(执行此循环的线程)应该使用 Begin 方法启动所有异步任务,然后再开始使用 End 方法处理结果。如果您在主调用线程中使用 Thread.Sleep() 来等待异步操作完成(就像您在这里一样),那么您做错了,最终发生的是每个命令,一次一个, 正在被调用,然后在下一个开始之前等待。

请尝试以下方法:

public void BeginExecutingCommands(Report someReport)
{
    foreach (record someRecord in someReport.r) 
    {
        var command = new SqlCommand("[procname]", sqlConn);

        command.CommandTimeout = 0;
        command.CommandType = CommandType.StoredProcedure;
        command.Parameters.Add(…);

        command.BeginExecuteReader(ReaderExecuted, 
            new object[] { command, someReport, someRecord });                   
    }
}

void ReaderExecuted(IAsyncResult result)
{
    var state = (object[])result.AsyncState;
    var command = state[0] as SqlCommand;
    var someReport = state[1] as Report;
    var someRecord = state[2] as Record;

    try
    {
        using (SqlDataReader reader = command.EndExecuteReader(result))
        {
            // work with reader, command, someReport and someRecord to do what you need.
        }
    }
    catch (Exception ex)
    {
        // handle exceptions that occurred during the async operation here
    }
}

【讨论】:

  • 我已从回调方法 (ReaderExecuted) 中删除了 public 可访问性修饰符。这些不应该是公开的,因为它们不是完整的操作,而只是另一种方法的逻辑“剩余部分”。
  • +1 用于演示正确使用 Begin…/End… 异步模式。但是,我不能 100% 确定这会解决主要问题。我也不确定线程​​池和数据库将如何处理可能 1,000 多个几乎同时的请求......?
  • 线程池将在它们进入时尽可能快地调度一定数量的它们,达到“最小”阈值。然后,当线程数高于该阈值时,它将每秒启动 4 个线程,直到达到最大阈值,此时它将保留任何新请求。最小和最大阈值是可配置的。
  • 至于数据库,我已经看到 MSSql 受到了相当大的冲击。我们的主数据库服务器有大约 24 个 EU,几乎可以处理我们的整个业务,除了报告(我们将事务日志发送到报告数据库以避免锁定问题)。我知道至少一个非锁定状态查询对我们的主数据库执行平均 100 次每秒。没问题。
  • 感谢您回答我的两个问题,我不知道。很高兴知道!
【解决方案4】:

在 SQL 中,写入的另一端是一个(一个)磁盘。你很少能并行写得更快。事实上,由于索引碎片,并行通常会减慢它的速度。如果您可以在加载之前按主(集群)键对数据进行排序。在大负载甚至禁用其他键时,加载数据重建键。

不太确定异步中在做什么,但可以肯定它没有按照您的预期进行,因为它正在等待自己。

try
{
    using (var command = new SqlCommand("[procname]", sqlConn))
    {
        command.CommandTimeout = 0;
        command.CommandType = CommandType.StoredProcedure;

        foreach (record someredord Somereport.r)
        {
            command.Parameters.Clear()
            command.Parameters.Add(…);

            using (var rdr = command.ExecuteReader())
            {
                while (rdr.Read())
                {
                    …
                }
            }
        }
    }
}
catch (…)
{
    …
}

【讨论】:

  • @stakx rdr.Close() 会解决这个问题吗?
  • IIRC rdr.Close() 将具有与 rdr.Dispose() 相同的效果...但 using 块比在 finally 子句中包装 rdr.Close() 更容易(您必须这样做为异常安全而做)。
  • 好的,但是在每个循环创建一个新的 rdr 的开销上。如果重用 rdr,Catch 上不会捕获什么异常?
  • 您实际上不能在多个查询中重复使用它们。出于所有实际目的,您应该假设对SqlCommand.ExecuteReader() 的每次调用都将返回SqlDataReader 的新实例。仅仅因为您使用相同的 变量 引用所有这些不同的实例,并不会以任何方式重用它们。
  • @stakx 我不得不说我不相信你,但我进行了一些测试,他们对你所说的没有异议,为了便于阅读,我喜欢 using 语法。
【解决方案5】:

正如我们在 cmets 中所讨论的,将这些数据存储在内存中并使用它可能是一种更有效的方法。

因此,一种简单的方法是从实体框架开始。 Entity Framework 将根据您的数据库模式自动为您生成类。然后你可以import a stored procedure 保存你的 SELECT 语句。我建议将存储过程导入 EF 的原因是,这种方法通常比在 LINQ 中针对 EF 进行查询更有效。

然后运行存储过程并将数据存储在List 中,就像这样......

var data = db.MyStoredProc().ToList();

然后你可以用data 做任何你想做的事。或者正如我所提到的,如果您要对主键进行大量查找,请使用 ToDictionary() 类似的东西...

var data = db.MyStoredProc().ToDictionary(k => k.MyPrimaryKey);

无论哪种方式,此时您都将在内存中使用您的 data

【讨论】:

    【解决方案6】:

    似乎执行您的SQL 命令会锁定一些必需的资源,这就是强制您使用Async 方法的原因(我的猜测)。

    如果数据库未在使用中,请尝试对其进行独占访问。即便如此,由于数据模型的复杂性存在一些内部事务,请考虑咨询数据库设计人员。

    【讨论】:

    • 谢谢。数据库实际上正在被使用,所以我不能按照你的建议做。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 2012-09-20
    • 2018-11-18
    • 2011-08-09
    • 1970-01-01
    • 1970-01-01
    • 2012-10-28
    相关资源
    最近更新 更多