代码之家  ›  专栏  ›  技术社区  ›  Community wiki

SqlConnections,Parallel.For,一个旧的C#应用程序,初始化SqlConnections时随机挂起

  •  7
  • Community wiki  · 技术社区  · 1 年前

    我的问题是:本周早些时候,我接到了加快我们项目中一项任务的任务。我看了一下,立刻想到了在该任务中为函数使用并行foreach循环的想法。

    我实现了它,完成了函数(包括所有子函数),并更改了SqlConnections(和其他东西),使其能够并行运行。我开始了整件事,一切进展得又好又快(仅此一项就将完成任务的时间减少了约45%)

    现在,昨天我们想用更多的数据来尝试同样的事情。。。我遇到了一些奇怪的问题:每当调用并行函数时,它就完成了它的工作。。。但有时其中一个线程会挂起至少4分钟(对于连接AND命令,超时设置为1分钟)。

    如果在此期间暂停程序,我会看到该循环中只有一个线程仍然处于活动状态,并且它挂起

    connection.Open()
    

    大约4分钟后,程序简单地继续运行,而不会引发错误(除了输出框中的一条消息,说某个地方发生了异常,但我的应用程序没有捕捉到它,而是在SqlConnection/SqlCommand对象中的某个地方)。

    我可以在不发生任何事情的情况下终止MSSQLServer上的所有连接,而且MSSQLServer在这4分钟内什么也不做,所有连接都是空闲的。

    这是用于向数据库发送更新/插入/删除语句的过程:

    int i = 80;
    bool itDidntWork = true;
    Random random = new Random();
    while (itDidntWork && i > 0)
    {
        try
        {
            using (SqlConnection connection = new SqlConnection(sqlConnectionString))
            {
                connection.Open();
                lock (connection)
                {
                    command.Connection = connection;
                    command.ExecuteNonQuery();
                }
    
                itDidntWork = false;
            }
        }
        catch (Exception ex)
        {
            if (ex is SqlException && ((SqlException)ex).ErrorCode == -2146232060)
            {
                Thread.Sleep(random.Next(500, 5000));
            }
            else
            {
                SqlConnection.ClearAllPools();
            }
    
            Thread.Sleep(random.Next(50, 110));
            i--;
            if (i == 0)
            {
                writeError(ex);
            }
        }
    }
    

    以防万一:在较小的数据库上可能会发生死锁(错误编号2146232060),所以如果发生死锁,我必须使冲突语句在不同的时间发生。即使在小型数据库/小型服务器上也能很好地工作。如果错误不是由死锁引起的,那么很可能是连接有故障,所以我正在清理所有断开的连接。

    存在用于执行标量、填充数据表/数据集的类似函数(是的,应用程序是 那个 旧的)和执行存储过程。

    是的,所有这些都用在了并行循环中。

    有人知道那里会发生什么吗?或者我想知道那里发生了什么?

    *编辑命令对象:

    它被赋予函数,当命令对象被赋予函数时,它总是一个新的对象。

    关于锁:如果我把锁收起来,我会收到成百上千的“连接已关闭”或“连接已打开”错误,因为open()函数只是从.NET的连接池中获取一个连接。锁确实按预期工作。

    示例代码:

    using(SqlCommand deleteCommand = new SqlCommand(sqlStatement))
    {
        ExecuteNonQuerySafely(deleteCommand); // that's the function that contains the body I posted above
    }
    

    *编辑2

    我得改正一下:它取决于这个

    command.Connection = connection;
    

    至少我想是的,因为当我暂停应用程序时,“step”标记thingi是绿色的

    command.ExecuteNonQuery();
    

    说这就是下一步要执行的语句。

    *编辑3 只是为了确保我刚刚开始了另一个测试,在连接对象周围没有任何锁。。。需要几分钟才能得到结果。

    *编辑4 嗯,我错了。我删除了锁定语句。。。它仍然有效。也许我第一次尝试它时,有一个重复使用的连接或其他什么。谢谢你指出这一点。

    *编辑5 我感觉这种情况只发生在对特定数据库过程的一次特定调用中。 我不知道为什么。就C#而言,该调用与其他调用没有区别 参见编辑6 。由于它当时没有执行该语句(我想。也许有人可以纠正我的错误。如果在调试模式下,一行标记为绿色(而不是黄色),它还没有执行该声明,而是等待该行之前的语句完成,这是正确的吗?)这很奇怪。

    *编辑6 有3个命令对象在整个时间内都被重用。它们是在平行函数之上定义的。我不知道那有多糟糕。它们只用于调用一个存储过程(每个存储过程称为不同的过程),当然具有不同的参数和新的连接(通过上述方法)。

    *编辑7 好吧,实际上只有当调用一个特定的存储过程时才会调用。除了它在挂起的连接对象的赋值上(下一行标记为绿色)。 试图弄清楚这是什么原因造成的。

    *编辑8 是的,这只是发生在另一个指挥部。就这样。

    *编辑9 好的。问题解决了。“挂起”实际上是CommandTimeouts,设置为10分钟(!)。它们只为两个命令设置(我在编辑7中提到的一个和我在编辑8中提到的那个)。由于我在重组我的命令以使其像devunde建议的那样时发现了这两个问题,我将他的答案标记为解决了我的问题的答案。此外,他关于限制for循环使用的线程数量的建议进一步加快了进程。

    特别感谢Marc Gravell解释了这些事情,并在周六和我一起呆在这里;)

    1 回复  |  直到 8 年前
        1
  •  4
  •   Marcelo De Zen    3 年前

    我认为问题可以在你的编辑6中找到: 编辑6:。。。3个命令对象一直被重复使用

    在并行循环中使用的任何数据都必须在循环中创建,或者必须有适当的同步代码,以确保一次只有一个线程可以访问该特定对象。我在里面看不到这样的代码 ExecuteNonQuerySafely 。 a) 锁定连接在那里没有效果,因为连接对象是在方法内部创建的。 b) 锁定命令并不能保证线程安全——可能是在将命令锁定在方法内部之前设置了命令参数。A. lock(command) 如果在调用 安全地执行非查询 然而,在并行循环中锁定并不是一件好事——毕竟这是反并行的定义,最好完全避免这种情况,并为每次迭代创建一个新命令。更好的方法是对 安全地执行非查询 ,它可以接受回调操作,而不是SqlCommand。示例:

    public void ExecuteCommandSafely(Action<SqlCommand> callback) {
       ... do init stuff ...
       using (var connection = new SqlConnection(...)) {
          using (var command = new SqlCommand() {
             command.Connection = connection;
             try{
                callback(command);
             }
             ... error handling stuff ...
          }          
    
       }
    
    }
    

    并使用:

    ExecuteCommandSafely((command) => {
       command.CommandText = "...";
       ... set parameters ..
       command.ExecuteNonQuery();
    });
    

    最后,并行执行命令时出现错误的事实表明,在这种情况下,并行执行可能不是一件好事。你在浪费服务器资源来获取错误。连接成本高昂,请尝试使用 MaxDegreeOfParalellism 选项来调整此特定循环的工作负载(请记住,最佳值将根据硬件/服务器/网络等而变化) Parallel.ForEach 方法具有一个重载,该重载接受 ParallelOptions 参数,您可以在其中设置要为该par并行执行的线程数 ( http://msdn.microsoft.com/en-us/library/system.threading.tasks.paralleloptions.maxdegreeofparallelism.aspx )。