More Effective C# 37.使用執行緒區集取代建立執行緒 More Effective C# 37.使用執行緒區集取代建立執行緒(Use the Thread Pool Instead of Creating Threads)

這個做法解釋 Task 背後的執行緒管理機制與手動建立執行緒對效能的影響。

由於寫出來的應用程式會運作在各式各樣的機器上,所以你很難在程式設計階段就直接決定可以使用多少執行緒數量, 所以手動建立執行緒會碰到最大的問題就是很難最佳化應用程式執行緒總數量。

所以就有了 Thread Pool 這個概念,讓它自行管理最適合的執行緒總數量,如果同時要求過多的執行緒,Thread Pool 會要求應用程式排隊等待, 直到有可用的執行緒釋出為止,官方目前推薦使用的 Task 類別就內建了 Thread Pool 機制。

下面這個範例測試了四種情況,分別是單一執行緒方法、以 Task 為主的方法、以 ThreadPool 為主的方法、手動建立 Thread 的方法。

void Main()
{
	Console.WriteLine(OneThread());
	Console.WriteLine(TaskLibrary(100));
	Console.WriteLine(ThreadPoolThreads(100));
	Console.WriteLine(ManualThreads(100));
}
public static int LowerBound { get; set; } = 1;
public static int UpperBound { get; set; } = 100000 * 1000;

public static class Hero
{
	public static double FindRoot(double number)
	{
		double previousError = double.MaxValue;
		double guess = 1;
		double error = Math.Abs(guess * guess - number);
		while (previousError / error > 1.000001)
		{
			guess = (number / guess + guess) / 2.0;
			previousError = error;
			error = Math.Abs(guess * guess - number);
		}
		return guess;
	}
}

private static double OneThread()
{
	Stopwatch start = new Stopwatch();
	double answer;
	start.Start();
	for (int i = LowerBound; i < UpperBound; i++)
		answer = Hero.FindRoot(i);
	start.Stop();
	return start.ElapsedMilliseconds;
}
private static async Task<double> TaskLibrary(int numTasks)
{
	var itemsPerTask = (UpperBound - LowerBound) / numTasks + 1;
	double answer;
	List<Task> tasks = new List<Task>(numTasks);
	Stopwatch start = new Stopwatch();
	start.Start();
	for (int i = LowerBound; i < UpperBound; i += itemsPerTask)
	{
		tasks.Add(Task.Run(() =>
		{
			for (int j = i; j < i + itemsPerTask; j++)
				answer = Hero.FindRoot(j);
		}));
	}
	await Task.WhenAll(tasks);
	start.Stop();
	return start.ElapsedMilliseconds;
}
private static double ThreadPoolThreads(int numThreads)
{
	Stopwatch start = new Stopwatch();
	using (AutoResetEvent e = new AutoResetEvent(false))
	{
		int workerThreads = numThreads;
		double answer;
		start.Start();
		for (int thread = 0; thread < numThreads; thread++)
			System.Threading.ThreadPool.QueueUserWorkItem(
				(x) =>
				{
					for (int i = LowerBound; i < UpperBound; i++)
						if (i % numThreads == thread)
							answer = Hero.FindRoot(i);
					if (Interlocked.Decrement(ref workerThreads) == 0)
						e.Set();
				});
		e.WaitOne();
		start.Stop();
		return start.ElapsedMilliseconds;
	}
}
private static double ManualThreads(int numThreads)
{
	Stopwatch start = new Stopwatch();
	using (AutoResetEvent e = new AutoResetEvent(false))
	{
		int workerThreads = numThreads;
		double answer;
		start.Start();
		for (int thread = 0; thread < numThreads; thread++)
		{
			Thread t = new Thread(() =>
				{
					for (int i = LowerBound; i < UpperBound; i++)
						if (i % numThreads == thread)
							answer = Hero.FindRoot(i);
					if (Interlocked.Decrement(ref workerThreads) == 0)
						e.Set();
				});
			t.Start();
		}
		e.WaitOne();
		start.Stop();
		return start.ElapsedMilliseconds;
	}
}

從這個測試可以看出需要建立大量執行緒的時候反而會造成效能瓶頸,並且 Task 在高負載與低負載的反應時間綜合起來看是最優秀的, 也得知並不是大量建立執行緒就能更快完成任務,

建立大量建立執行緒的場景使用 Thread Pool 與手動建立執行緒相比會快很多,是因為 Thread Pool 完成工作後並不會馬上銷毀執行緒, 而是會在 Thread Pool 等待下一個任務,但手動建立執行緒每次就需要經過建立與銷毀的過程所以需要花費更多時間。

另外是當建立過多執行緒時 Thread Pool 會自動管理執行緒上限,如果超過就需要排隊等待有空閒的執行緒出現。


Summary

由於手動建立執行緒與透過 Thread Pool 取得執行緒在現代的 C# 已經沒必要使用,除非有特殊的應用場景否則平常使用 Task 就能滿足需求。