我的问题:本周早些时候,我完成了加速我们计划任务的任务。我看着它,立即想到了在该任务中使用并行foreach循环来实现一个功能。
我实现了它,经历了函数(包括所有子函数)并更改了SqlConnections(以及其他东西),因此它可以并行运行。我开始了所有的事情,一切顺利而且快速(单独将这项任务的时间缩短了约45%)
现在,昨天我们想用更多的数据尝试同样的事情......我遇到了一些奇怪的问题:每当调用并行函数时,它就完成了它的工作......但有时其中一个线程会挂起至少4分钟(超时设置为一分钟,连接AND命令)。
如果我在此期间暂停程序,我发现该循环中只有一个线程仍然处于活动状态且挂起
connection.Open()
大约4分钟之后,程序只是继续而不会抛出错误(除了输出框中的消息,说某个地方发生了异常,但它没有被我的应用程序捕获,而是在SqlConnection / SqlCommand对象中的某处)
我可以杀死MSSQLServer上的所有连接而不会发生任何事情,MSSQLServer在这4分钟内什么都不做,所有连接都是空闲的。
这是用于将Update / Insert / Delete语句发送到数据库的过程:
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#wise在该呼叫和其他呼叫之间没有区别 参见编辑6 。因为它在那时没有执行语句(我猜。也许有人可以纠正我。如果在调试模式下,一行是绿色标记(而不是黄色)它还没有执行该语句但是等待对于该行之前的陈述,这是正确的吗?)这很奇怪。
*编辑6 有3个命令对象一直被重用。它们在并行函数之上定义。我不知道是多么糟糕/是。它们只用于调用一个存储过程(每个存储过程称为不同的过程),当然使用不同的参数和新的连接(通过上述方法)。
*编辑7
好吧,只有在调用一个特定的存储过程时才会这样。除了它挂起的连接对象的分配(下一行标记为绿色)。
试图弄清楚原因是什么。
*编辑8 是的,它发生在另一个命令。就是这样。
*编辑9 好。问题解决了。 'hangs'实际上是CommandTimeouts,设置为10分钟(!)。它们只设置了两个命令(我在编辑7中提到的那个和我在编辑8中提到的那个)。因为我在重构命令时发现了这两个命令,使它们像devundef建议的那样,我将他的答案标记为解决我问题的答案。此外,他建议限制我的for-loop使用的线程数量,甚至更多。
特别感谢Marc Gravell在星期六和我一起在这里解释东西;)
答案 0 :(得分:3)
我认为问题可以在编辑6中找到:编辑6有3个命令对象一直被重复使用。
并行内使用的任何数据都必须在循环内创建,或者必须具有正确的同步代码,以保护它们同时由2个或多个线程使用。我在ExecuteNonQuerySafely
中没有看到此代码。锁定连接无效,因为在方法内创建了连接对象。锁定命令不能保证线程安全,因为可能是在将命令参数锁定在方法内之前设置命令参数。如果在调用lock(command)
之前锁定命令,ExecuteNonQuerySafely
将起作用,但在并行循环内锁定并不是一件好事,最好避免这种情况并为每次迭代创建一个新命令。更好的是在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();
});
最后,您在并行执行命令时遇到错误的事实表明,在这种情况下并行执行可能不是一件好事。您浪费服务器(sql / web ...)资源来获取错误,并且连接很昂贵。尝试使用MaxDegreeOfParalellism
选项来调整此特定循环的工作负载(记住最佳值将根据硬件/服务器/网络/等进行更改。)。 Parallel.ForEach
方法具有接受ParallelOptions
参数的重载,您可以在其中配置要执行的线程数(http://msdn.microsoft.com/en-us/library/system)。 threading.tasks.paralleloptions.maxdegreeofparallelism.aspx)。