任务并行库和SQL连接

时间:2013-09-19 15:50:48

标签: sql-server task-parallel-library connection-pooling

我希望有人可以通过TPL和SQL连接来确认实际发生的事情。

基本上,我有一个大型应用程序,实质上是从SQL Server读取一个表,然后按顺序处理每一行。每行的处理可能需要相当长的时间。所以,我想改变它以使用任务并行库,在数据表中的行之间使用“Parallel.ForEach”。这似乎工作了一段时间(分钟),然后这一切都变成了梨形... ...

“从池中获取连接之前已经过了超时时间。这可能是因为所有池连接都在使用中并且达到了最大池大小。”

现在,我推测以下(当然可能完全错误)

“ForEach”为每一行创建任务,根据核心数量(或其他)创建一些限制。让我们说4是因为想要一个更好的主意。四个任务中的每一个都获得一行,然后开始处理它。 TPL一直等到机器不太忙,然后又开火了。我期待最多四个。

但这不是我观察到的 - 而不是我认为正在发生的事情。

所以...我写了一个快速测试(见下文)

Sub Main()
    Dim tbl As New DataTable()

    FillTable(tbl)

    Parallel.ForEach(tbl.AsEnumerable(), AddressOf ProcessRow)

End Sub

Private n As Integer = 0

Sub ProcessRow(row As DataRow, state As ParallelLoopState)
    n += 1 ' I know... not thread safe
    Console.WriteLine("Starting thread {0}({1})", n, Thread.CurrentThread.ManagedThreadId)
    Using cnx As SqlConnection = New SqlConnection(My.Settings.ConnectionString)
        cnx.Open()
        Thread.Sleep(TimeSpan.FromMinutes(5))
        cnx.Close()
    End Using
    Console.WriteLine("Closing thread {0}({1})", n, Thread.CurrentThread.ManagedThreadId)
    n -= 1
End Sub

这创造了比我对任务数量的猜测更多的方法。因此,我猜测TPL将任务激活到它认为会让我的机器忙碌的极限,但是,嘿,这是什么,我们在这里不是很忙,所以让我们开始更多。仍然不是很忙,所以...等等(看起来像是一个新的任务 - 大致)

这是合情合理的,但我希望它能在30秒(SQL连接超时)之后,如果它获得100个打开的SQL连接 - 默认的连接池大小 - 它不会“T

因此,为了稍微缩小它,我改变了我的连接字符串以限制最大池大小。

Sub Main()
    Dim tbl As New DataTable()

    Dim csb As New SqlConnectionStringBuilder(My.Settings.ConnectionString)
    csb.MaxPoolSize = 10
    csb.ApplicationName = "Test 1"
    My.Settings("ConnectionString") = csb.ToString()

    FillTable(tbl)

    Parallel.ForEach(tbl.AsEnumerable(), AddressOf ProcessRow)

End Sub

我计算到SQL服务器的实际连接数,并且正如预期的那样,它是10.但是我的应用程序启动了26个任务 - 然后挂起。因此,设置SQL的最大池大小会以某种方式将任务数量限制为26,但为什么没有27,尤其是为什么它不会因为池已满而在11处失败?

显然,在某个地方,我要求的工作量比我的机器要多,我可以在ForEach中添加“MaxDegreesOfParallelism”,但我对这里的实际情况感兴趣。

PS。

实际上,在完成(我猜测)5分钟的26项任务之后, 会因原始(达到最大池大小)错误而失败。嗯?

感谢。

修改1:

实际上,我现在认为在任务中发生的事情(我的“ProcessRow”方法)是在10次成功连接/任务之后,第11次确实阻止连接超时,然后确实得到原始异常 - 与任何后续任务一样。

所以...我得出结论,TPL在大约1秒创建任务,并且在任务11抛出异常之前,它有足够的时间创建大约26/27。然后所有后续任务也会抛出异常(大约相隔一秒)并且TPL停止创建新任务(因为它在一个或多个任务中得到未处理的异常?)

出于某种原因,(尚未确定),ForEach会挂起一段时间。如果我修改我的ProcessRow方法以使用状态来说“停止”,它似乎没有效果。

Sub ProcessRow(row As DataRow, state As ParallelLoopState)
    n += 1
    Console.WriteLine("Starting thread {0}({1})", n, Thread.CurrentThread.ManagedThreadId)
    Try
        Using cnx As SqlConnection = fnNewConnection()
            Thread.Sleep(TimeSpan.FromMinutes(5))
        End Using
    Catch ex As Exception
        Console.WriteLine("Exception on thread {0}", Thread.CurrentThread.ManagedThreadId)
        state.Stop()
        Throw
    End Try
    Console.WriteLine("Closing thread {0}({1})", n, Thread.CurrentThread.ManagedThreadId)
    n -= 1
End Sub

编辑2:

Dur ...长时间延迟的原因是,虽然任务11以后全部崩溃和燃烧,但任务1到10没有,并且所有人都坐在那里睡了5分钟。 TPL已停止创建新任务(因为创建的一个或多个任务中的未处理异常),然后等待未崩溃的任务完成

2 个答案:

答案 0 :(得分:1)

原始问题的编辑增加了更多细节,最终,答案变得明显。

TPL重复创建任务,因为 创建的任务是(基本上)空闲。这一点很好,直到连接池耗尽,此时想要新连接的任务等待一个可用的任务,并超时。与此同时,TPL仍在创造更多任务,所有任务都注定要失败。连接超时后,任务开始失败,随后的异常导致TPL停止创建新任务。然后,在抛出AggregateException之前,TPL会等待完成连接的任务。

答案 1 :(得分:0)

TPL不适用于IO绑定工作。它具有启发式功能,可用于控制活动线程的数量。对于长时间运行和/或IO绑定的任务,这些启发式失败,导致它注入越来越多的线程而没有实际限制。

使用PLINQ使用WithDegreeOfParallelism设置固定数量的线程。您应该测试不同的金额。 It could look like this.我已经在SO上写了很多关于这个主题的文章,但我现在找不到它。

我不知道你为什么在你的例子中看到完全 26个线程。请注意,当池耗尽时,连接请求仅在超时后失败。整个系统非常不确定,我认为任何数量的线程都是合理的。