关于线程
我建议避免自己显式地创建线程。
更好的选择是使用ThreadPool.QueueUserWorkItem
如果你使用.Net 4.0,则可以使用更强大的任务并行库,它还允许您以更强大的方式使用线程池线程(值得一看的是Task.Factory.StartNew
)
如果我们选择显式创建线程会怎样?
假设您的list[0].Count返回1000项。我们还假设您正在高端(在撰写本文时)的16核计算机上执行此操作。直接的影响是我们有1000个线程争夺这些有限的资源(16个内核)。
任务越多且每个任务运行时间越长,就会花费更多的时间进行上下文切换。此外,创建线程是昂贵的。如果使用重用现有线程的方法,则可以避免显式创建每个线程的开销。
因此,虽然多线程的初始意图是为了提高速度,但我们可以看到它可能会产生相反的效果。
如何克服“过度”线程?
这就是ThreadPool
发挥作用的地方。
线程池是一组线程,可用于后台执行许多任务。
它们是如何工作的:
一旦池中的线程完成其任务,它将返回到等待线程队列中,以便可以重复使用。此重用使应用程序避免为每个任务创建新线程的成本。
线程池通常具有最大线程数。如果所有线程都忙碌,则额外的任务将被放置在队列中,直到线程变得可用。
因此,通过使用线程池线程,我们可以更有效地利用资源。
- 为了最大化实际工作量,我们不会使处理器过度饱和,这样可以减少线程之间的切换时间,从而更多地执行线程应该完成的代码。
- 更快的线程启动:每个线程池线程都是立即可用的,而不是等待新线程被构建。
- 为了最小化内存消耗,线程池将限制线程数量到线程池大小,并排队任何超出线程池大小限制的请求。(请参阅
ThreadPool.GetMaxThreads
)。这种设计选择的主要原因,当然是为了避免过多的线程请求过度饱和有限数量的核心,从而保持上下文切换到较低水平。
太多理论,让我们把所有这些理论付诸实践!
好的,在理论上了解这一切很好,但让我们将其付诸实践并查看数字告诉我们的结果。我们将使用一个简化的应用程序的基本版本进行比较,以粗略地指示数量级的差异。我们将对比新线程、线程池和任务并行库(TPL)之间的差异。
新线程
static void Main(string[] args)
{
int itemCount = 1000;
Stopwatch stopwatch = new Stopwatch();
long initialMemoryFootPrint = GC.GetTotalMemory(true);
stopwatch.Start();
for (int i = 0; i < itemCount; i++)
{
int iCopy = i;
Thread thread = new Thread(() =>
{
int k = 0;
while (true)
{
if (k++ > 100000)
break;
}
if ((iCopy + 1) % 200 == 0)
Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
});
thread.Name = "SID" + iCopy;
thread.Start();
}
Console.ReadKey();
Console.WriteLine(GC.GetTotalMemory(false) - initialMemoryFootPrint);
Console.ReadKey();
}
结果:
![New Thread Benchmark](https://istack.dev59.com/zKfAn.webp)
ThreadPool.EnqueueUserWorkItem
static void Main(string[] args)
{
int itemCount = 1000;
Stopwatch stopwatch = new Stopwatch();
long initialMemoryFootPrint = GC.GetTotalMemory(true);
stopwatch.Start();
for (int i = 0; i < itemCount; i++)
{
int iCopy = i;
ThreadPool.QueueUserWorkItem((w) =>
{
int k = 0;
while (true)
{
if (k++ > 100000)
break;
}
if ((iCopy + 1) % 200 == 0)
Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
});
}
Console.ReadKey();
Console.WriteLine("Memory usage: " + (GC.GetTotalMemory(false) - initialMemoryFootPrint));
Console.ReadKey();
}
结果:
![ThreadPool基准测试](https://istack.dev59.com/A8uCX.webp)
任务并行库(TPL)
static void Main(string[] args)
{
int itemCount = 1000;
Stopwatch stopwatch = new Stopwatch();
long initialMemoryFootPrint = GC.GetTotalMemory(true);
stopwatch.Start();
for (int i = 0; i < itemCount; i++)
{
int iCopy = i;
Task.Factory.StartNew(() =>
{
int k = 0;
while (true)
{
if (k++ > 100000)
break;
}
if ((iCopy + 1) % 200 == 0)
Console.WriteLine(iCopy + " - Time elapsed: (ms)" + stopwatch.ElapsedMilliseconds);
});
}
Console.ReadKey();
Console.WriteLine("Memory usage: " + (GC.GetTotalMemory(false) - initialMemoryFootPrint));
Console.ReadKey();
}
结果:
![Task Parallel Library result](https://istack.dev59.com/BWSCu.webp)
因此我们可以看到:
+
| | new Thread | ThreadPool | TPL |
+
| Time | 6749 | 228ms | 222ms |
| Memory | ≈300kb | ≈103kb | ≈123kb |
+
以上内容符合我们在理论上预期的情况。与线程池相比,新线程需要更高的内存,并且总体性能较慢。线程池和TPL具有相同的性能,但TPL的内存占用略高于纯线程池,但考虑到任务所提供的额外灵活性(例如取消,等待完成查询任务状态),这可能是一个值得付出的代价。
到此为止,我们已经证明使用线程池线程是速度和内存方面更可取的选项。
不过,我们还没有回答你的问题。如何追踪正在运行的线程的状态。
回答您的问题
根据我们收集的见解,以下是我处理它的方式:
List<string>[] list = listdbConnect.Select()
int itemCount = list[0].Count;
Task[] tasks = new Task[itemCount];
stopwatch.Start();
for (int i = 0; i < itemCount; i++)
);
}
Task.WaitAll(tasks);
tasks[1].IsCanceled
tasks[1].IsCompleted
tasks[1].IsFaulted
tasks[1].Status
最后一点需要注意的是,在Thread.Start中不能使用变量i,因为它会创建一个闭包并共享所有线程中正在更改的变量。如果需要访问i,可以通过创建变量的副本并传递该副本来解决这个问题,这将使每个线程都有一个闭包,从而使其线程安全。
祝你好运!