如何正确限制多线程应用程序

本文关键字:多线程 应用程序 何正确 | 更新日期: 2023-09-27 18:33:43

我有一个在64位Windows 2008 r2服务器上运行的c#控制台应用程序,该服务器还托管MSSQL Server 2005。

此应用程序运行文本文件,读取行,将行值拆分为变量,并将数据插入到托管在 localhost 的 SQL 数据库中。

每个

文本文件都是一个新线程,每一行都是一个新线程,每个 SQL insert 语句都在一个新线程下执行。

我正在计算每种类型的线程的数量,并在它们完成时递减。我想知道最好的方法是"挂起"未来的线程打开......

例如.. 在打开新的 SQL 插入线程之前,我正在调用...

while(numberofcurrentthreads > specifiednumberofthreads)
{
// wait
}
new.Thread(insertSQL);

其中指定的线程数已估计为不会抛出 System.OutofMemoryExceptions 的值。为了确定每个过程的数字,已经进行了大量的猜测工作。

我的问题是..有没有更"有效"或更合适的方法来做到这一点?有没有办法读取系统内存,而不是物理内存,并根据指定的资源分配进行等待?

为了说明这个想法...

while(System.Memory < (System.Memory/2) || System.OutofMemory == true)
{
// wait
}
new.Thread(insertSQL);

我目前采用的方法可以在相当长的时间内完成......但它可以做得更好。正在经历该过程的某些文本文件比其他文本文件大,并且不一定充分利用系统资源...

例如,如果我说一次处理 2 个文本文件,当两个文本文件都<300KB 时,它可以完美运行。如果一两个超过 100,000KB,它就不能很好地工作。

似乎还有一个"黄油区",在那里事情处理效率最高。平均约占所有 CPU 资源的 75%。将这些值调得太高,它将以 100% CPU 运行,但处理速度较慢,因为它无法跟上。

如何正确限制多线程应用程序

为每个文件、每一行和每个 SQL 插入语句创建一个新线程是疯狂的。使用三个线程和一个链式生产者-消费者模型可能会好得多,所有这些模型都通过线程安全队列进行通信。在C#中,这将是BlockingCollection。

首先,设置两个队列,一个用于从文本文件中读取的行,另一个用于已处理的行:

const int MaxQueueSize = 10000;
BlockingCollection<string> _lines = new BlockingCollection<string>(MaxQueueSize);
BlockingCollection<DataObject> _dataObjects = new BlockingCollection<DataObject>(MaxQueueSize);

顺便说一下,DataObject就是我将要插入到数据库中的对象。你不说那是什么。对于此讨论的目的来说,这并不重要,但您可以将其替换为用于表示已处理字符串的任何类型。

现在,您创建三个线程:

  1. 逐行读取文本文件并将行放入_lines队列的线程。
  2. 一个行处理器,它从_lines队列中逐个读取行,对其进行处理,并创建一个DataObject,然后将其放置在_dataObjects队列中。
  3. 读取_dataObjects队列并将其插入数据库的线程。

除了简单(这很容易组合在一起(之外,这种模型还有很多好处。

首先,同时从磁盘读取多个线程通常会导致性能降低,因为磁盘驱动器一次只能执行一项操作。让多个线程同时击中磁盘只会导致不必要的磁头搜索。只需一个线程即可使您的输入队列保持已满。

其次,限制队列的大小将防止内存不足。当磁盘读取线程尝试将第 10,001 个项目插入队列时,它将等待,直到处理线程删除项目。这就是BlockingCollection的"阻塞"部分。

您可能会发现,您可以通过对 SQL 插入进行分组并一次发送一堆记录来加快 SQL 插入速度,这本质上是

一次批量插入 100 或 1000 条记录,而不是发送 100 或 1000 个单独的事务。

此解决方案可防止线程过多的问题。您有固定数量的线程,所有这些线程都尽可能快地运行。内存使用受到限制队列中可以包含的内容数量的限制。

该解决方案的扩展性也相当好。如果文件位于多个驱动器上,则可以添加第二个文件读取线程以从该其他物理驱动器读取文件,并将这些行放在同一队列中。 BlockingCollection支持多个生产者和多个消费者,因此添加另一个生产者一点也不麻烦。

消费者也是如此。如果发现处理步骤是瓶颈,可以添加另一个处理线程。它也将从_lines队列读取并写入dataObjects队列。

但是,线程数多于处理器内核数可能会使程序变慢。如果你有一个四核处理器,创建 8 个处理线程对你没有任何好处。这将使事情变慢,因为操作系统将花费大量时间在线程上下文切换上,而不是在做有用的工作上。

您必须进行一些调整才能获得最佳性能。队列大小应足够大以支持连续工作流(因此没有线程缺少工作,或花费太多时间等待输出队列(,但不能太大以溢出内存。根据三个阶段的相对速度,其中一个队列可能必须大于另一个队列。如果三个阶段中的一个是瓶颈,则可以在该阶段添加另一个线程来提供帮助。

我创建了此模型的简单示例,使用文本文件进行输入和输出。根据您的情况进行扩展应该很容易。请参阅简单多线程处理和后续工作,第 2 部分。