SqlConnection是什么?Parallel.For有什么作用?旧的C#应用程序在初始化SqlConnection时随机挂起是为什么?

7
我的问题:本周早些时候,我接到了一个任务,需要加速我们程序中的一个任务。我看了一下,立刻想到可以使用并行 foreach 循环来处理该任务中的一个函数。
我实现了这个想法,遍历了整个函数(包括所有子函数),并更改了 SqlConnections(和其他内容),以使其能够并行运行。我启动了整个程序,一切都很顺利和快速(仅这样就将该任务的时间缩短了约 45%)。
现在,昨天我们想尝试在更多数据上尝试同样的方法...但是我遇到了一些奇怪的问题:每当并行函数被调用时,它会执行它的工作...但有时线程中的一个会挂起至少 4 分钟(连接和命令的超时时间均为一分钟)。
如果我在此期间暂停程序,则会发现只有一个线程仍然活动,并且它在某处挂起。
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
}

我需要进行一些更正:它卡在这里。
command.Connection = connection;

至少我认为是这样的,因为当我暂停应用程序时,“步骤”标记会变成绿色并打开。
command.ExecuteNonQuery();

说这是下一个将要执行的语句。

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

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

*编辑5 我有一种感觉,这只发生在对特定数据库过程的特定调用中。我不知道为什么。在C#方面,该调用与其他调用之间没有区别请参见编辑6。由于它在那个时候没有执行语句(我猜测。也许有人可以纠正我。如果在调试模式下,一行代码被标记为绿色(而不是黄色),则它还没有执行该语句,而是等待该行代码之前的语句完成,这正确吗?)所以很奇怪。

*编辑6 有3个命令对象一直被重用。它们在并行函数上面被定义。我不知道这有多糟糕/多少好。它们仅用于调用一个存储过程(每个存储过程都调用不同的存储过程),当然使用不同的参数和新连接(通过上述方法)。

*编辑7 好吧,只有在调用一个特定的存储过程时才会发生。除此之外,它卡在了连接对象的赋值上(下一行标记为绿色)。 正在尝试弄清楚原因。

*编辑8 耶,刚刚又在另一个命令中出现了。就是这样。

*编辑9 好了。问题解决了。'卡住'实际上是设置为10分钟的CommandTimeouts。它们仅适用于两个命令(我在编辑7和编辑8中提到的命令)。由于我在重组我的命令以使它们像devundef建议的那样时找到了它们两个,所以我将他的答案标记为解决我的问题。此外,他建议限制我的for循环使用的线程数量,这进一步加快了进程速度。 特别感谢Marc Gravell在星期六解释事情并与我保持联系;)


6
在本地连接上进行锁定(以便实际上不会锁定任何东西),并在多个线程中重用共享的 SqlCommand,这听起来有些可疑。 - Mormegil
1
“lock (connection)” 没有任何作用,为了简单起见,请将其删除。我不知道“command”来自哪里,它不是共享的,对吗?您是否尝试监视持有连接对象的数量(在打开时增加共享计数,在关闭时减少),以查看它与池最大值相比有多高? - Jon Hanna
1
@SteffenWinkler 这取决于隔离级别;我猜它们是键范围锁,所以是的:它们可能会影响其他行。当然,允许使用更广泛的锁,但通常不会除非你要进行大规模更新或明确要求这样做。 - Marc Gravell
1
@Steffen 不是的;当你调用Open()时,它会从池中获取一个底层连接。但你没有锁定底层连接 - 你锁定的是SqlConnection:完全不同。SqlConnection不是来自池中;它是“新的”。 - Marc Gravell
1
只要它不被两个线程同时使用,这应该是没问题的。问题在于:你所描述的情况非常不典型,并且从发布的代码中并不明显。听起来很微妙。不幸的是,我怀疑有人需要能够真正重现这个问题才能进行调试,但我无论如何都想不出如何才能重现它。 - Marc Gravell
显示剩余11条评论
1个回答

4
我认为问题可以在您的第6次编辑中找到:edit 6: ...3个命令对象一直被重用
任何在并行循环中使用的数据必须在循环内创建,或者必须有适当的同步代码来确保每次只有1个线程访问该特定对象。我没有看到在ExecuteNonQuerySafely中有这样的代码。 a)锁定连接在那里没有效果,因为连接对象是在方法内创建的。 b)锁定命令将不能保证线程安全-可能在方法内锁定命令参数之前设置了命令。如果在调用ExecuteNonQuerySafely之前锁定命令,则lock(command)将起作用,但是在并行循环中使用锁定不是一个好主意-这是反并行的定义,最好完全避免这种情况,并为每个迭代创建一个新命令。更好的方法是对ExecuteNonQuerySafely进行一些重构,它可以接受回调操作而不是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参数,您可以在其中设置要并行执行的线程数。 (http://msdn.microsoft.com/en-us/library/system.threading.tasks.paralleloptions.maxdegreeofparallelism.aspx)。

1
嗨,我用命令对象重新设计了东西,所以那不再是问题了(所有命令对象现在真的只使用一次)。感谢代码片段,我想我会用它们;)非常感谢ParallelOptions的事情!之前不知道。MaxDegreeOFParalellism必须设置整个服务器还是我可以将其添加到连接字符串中?另外,据我所知,它“只是”限制服务器可以为一个查询使用多少线程,对或错? - Steffen Winkler
1
你说得对,并行选项是针对每个上下文的,你可以独立地为每个ForEach/For等设置选项,它不是一个全局设置。当你调用Parallel.ForEach时,你可以传递一个ParallelOptions参数给它,指定你想要并行运行多少个线程。 - Marcelo De Zen
1
@Steffen,坦率地说,根本没有重用命令对象的好处。我只会将它们保持在本地。问题解决了 :) - Marc Gravell
1
好的,我确实通过限制线程数量获得了更好的性能。此外,这也是我将您的答案标记为“答案”的原因:通过按照您展示的方式重新构建命令调用,我发现有两个命令(导致问题的那些命令)设置了CommandTimeout为600(即10分钟)。唉。将其设置为60秒后,我得到了超时而不是“挂起”。 - Steffen Winkler
1
指定并行计划执行时可使用的最大处理器数量,最多可达32个。这就是我的意思。两个选项具有相同的名称,因此我感到困惑。 - Steffen Winkler
显示剩余5条评论

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