zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C# (江湖小新)- 多线程之线程池 (线程也有专门的管理部门)

c#多线程线程 管理 部门 江湖 专门
2023-09-11 14:14:49 时间

线程池的基本概念

什么是线程池?

  •  .NET Framework的ThreadPool类提供一个线程池
  • “线程池”是可以用来在后台执行多个任务的线程集合
  • 线程池通常用于服务器应用程序。 每个传入请求都将分配给线程池中的一个线程,因此可以异步处理请求,而不会占用主线程,也不会延迟后续请求的处理
  • 一旦池中的某个线程完成任务,它将返回到等待线程队列中,等待被再次使用。 这种重用使应用程序可以避免为每个任务创建新线程的开销
  • 线程池通常具有最大线程数限制。 如果所有线程都繁忙,则额外的任务将放入队列中,直到有线程可用时才能够得到处理

为什么要用线程池?

  • 线程是非常消耗资源的,如果我们每次需要子线程来执行任务,就去创建一个新的线程,那当我们执行1千次1万次甚至是100万次的时候,那么电脑的资源消耗就非常严重,甚至承受不了
  • 线程池可以避免大量的创建和销毁的开支,具有更好的性能和稳定性
  • 开发人员把线程交给系统管理,可以集中精力处理其他任务

前台线程&后台线程

  • 前台线程: 当程序运行起来后,主线程已经运行结束了,但是程序却没有停止,子线程依然在运行
  • 后台线程:只要主线程和所有的前台线程执行结束,就算后台线程的任务还没完成,也会强行打断直接退出
  • 特点:后台适用于不太重要的任务,即被中断了也没事的。  前台线程适用于比较重要的任务,必须等任务执行完程序才能关闭
  • ThreadPool 线程池中的线程都是 后台线程

线程池的使用

设置线程池大小

  • ThreadPool.SetMaxThreads (int workerThreads,int completionPortThreads)
  • ThreadPool.SetMinThreads (int workerThreads,int completionPortThreads)
  • 参数解析:
    • completionPortThreads:线程池中异步 I/O 线程的数目 (I/O 完成线程)
    • workerThreads:线程池中辅助线程的数目(工作线程)
  • 对于以上两个参数的解释,摘自网络其它博客:
    • 工作者线程
      • 用来完成一些计算的任务,在任务执行的过程中,需要CPU不间断地处理,所以,在工作者线程的执行过程中,CPU和线程的资源是充分利用的
      • .NET中的术语工作者线程指的是任何线程而不是仅仅主线程
    • I/O线程
      • 主要用来完成输入和输出的工作,在这种情况下, 计算机需要I/O设备完成输入和输出的任务。在处理过程中,CPU是不需要参与处理过程的此时正在运行的线程将处于等待状态只有等任务完成后才会有事可做, 这样就造成线程资源浪费的问题。为了解决这样的问题,可以通过线程池来解决这样的问题,让线程池来管理线程
      • .NET中的一些API方法,通过APM(异步编程模式),内部实现了ThreadPool.BindHandle方法。BeginXXX方法将用户的回调委托送到某个设备驱动程序,然后返回线程池。
        当做完成后,OS会通过IOCP提醒CLR它工作已经完成,当接收到通知后,I/O线程会醒来并且运行用户的回调
      • 所以工作线程由开发人员调用,I/O线程由CLR调用。所以通常情况下,开发者并不会直接用到它。因此可以认为,工作者线程和I/O线程没有区别,它们都是普通的线程但是CLR线程池中区分它们的目的是为了避免线程都去处理I/O回调而被耗尽,从而引发死锁。(设想,所有的工作者线程每一个都去等待I/O异步完成。)

启动线程任务使用: QueueUserWorkItem()方法

  • static bool QueueUserWorkItem(WaitCallback callBack):参数为一个带一个object类型参数的委托,最后返回bool值成功则返回true

class ThreadPoolTest
{
	static void Main()
	{
		Console.WriteLine("启动多线程...");
		for(int i = 0; i <10; i++)
		{
			ThreadPool.QueueUserWorkItem( p => printStr("当前线程") );
		}
		Console.WriteLine("结束多线程...");
		Console.ReadKey();
	}

	private static void printStr(string str)
	{
		Console.WriteLine("{0}是:{1}", str, Thread.CurrentThread.ManagedThreadId);
	}
}

输出结果, 可以看到有很多线程ID是重复的,这就是线程池的强大之处了

启动多线程...
结束多线程...
当前线程是:8
当前线程是:6
当前线程是:10
当前线程是:9
当前线程是:7
当前线程是:11
当前线程是:12
当前线程是:9
当前线程是:6
当前线程是:10

查看最大/最小线程数 && 设置最大/最小线程数

class ThreadPoolTest
{
	static void Main()
	{
		// 获取默认的线程池中的最大,最小线程数
		ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
		Console.WriteLine("最大线程数,工作线程:{0}, IO线程数:{1}", maxWorkerThreads, maxCompletionPortThreads);
		ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
		Console.WriteLine("最小线程数,工作线程:{0}, IO线程数:{1}", minWorkerThreads, minCompletionPortThreads);

		// 重新设置最大、最小线程数
		ThreadPool.SetMaxThreads(10, 10);
		ThreadPool.SetMinThreads(3, 3);
		// 获取默认的线程池中的最大,最小线程数
		ThreadPool.GetMaxThreads(out int newMaxWorkThread, out int newMaxIOThread);
		Console.WriteLine("重新设置后的最大线程数,工作线程:{0}, IO线程数:{1}", newMaxWorkThread, newMaxIOThread);
		ThreadPool.GetMinThreads(out int newMinWorkThread, out int newMinIOThread);
		Console.WriteLine("重新设置后的最小线程数,工作线程:{0}, IO线程数:{1}", newMinWorkThread, newMinIOThread);


		Console.WriteLine("启动多线程...");
		for(int i = 0; i <10; i++)
		{
			ThreadPool.QueueUserWorkItem( p => printStr("当前线程") );
		}
		Console.WriteLine("结束多线程...");
		Console.ReadKey();
	}

	private static void printStr(string str)
	{
		Console.WriteLine("{0}是:{1}", str, Thread.CurrentThread.ManagedThreadId);
	}
}

输出结果

最大线程数,工作线程:32767, IO线程数:1000
最小线程数,工作线程:8, IO线程数:8
重新设置后的最大线程数,工作线程:10, IO线程数:10
重新设置后的最小线程数,工作线程:3, IO线程数:3
启动多线程...
结束多线程...
当前线程是:6
当前线程是:9
当前线程是:7
当前线程是:8
当前线程是:11
当前线程是:10
当前线程是:12
当前线程是:11
当前线程是:7
当前线程是:9

ThreadPool.SetMaxThreads的默认值

  • 它取决于.NET框架版本,在2.0,3.0和4.0中进行了更改。 在2.0中它是核心数量的50倍。 在3.0(又名2.0 SP1)中,它是内核数量的250倍,4.0根据位数和操作系统资源使其动态化。 默认 Max I / O完成线程是1000
  • 一般来说,它是非常的高,一个程序永远不会接近

使用以上方法设置线程数据大小时需注意

  • 不能将最大工作线程数或 I/O 完成线程数设置为小于计算机上的处理器数的数字
  • 不能将最大工作线程数或 I/O 完成线程数设置为小于相应最小工作线程数或 I/O 完成线程数的数字
  • 默认情况下,最小线程数设置为系统上的处理器数。 可以使用该方法 SetMinThreads 增加最小线程数。 但是,不必要地增加这些值可能导致性能问题。 如果在同一时间开始太多的任务,则所有任务均可能会很慢。 在大多数情况下,线程池将使用自己的算法更好地分配线程。 将最小处理器减少到小于处理器数也会损害性能

查看系统cpu的处理数

线程等待

由于线程池中的线程都是后台线程,当主线程及所有前台线程执行完时,后台线程就会被中断,但如果我们希望 主线程等待 线程池执行完成任务后再关闭程序 怎么办呢?

需要使用到ManualResetEvent类

ManualResetEvent需要一个bool类型的参数来表示暂停和停止 

class ThreadPoolTest2
{
	// 参数设置为false
	static ManualResetEvent manualResetEvent = new ManualResetEvent(false);
   
	static void Main()
	{
		Console.WriteLine("启动多线程...");
		ThreadPool.QueueUserWorkItem(p => printStr("当前线程"));
		// 等待 线程池执行任务完成
		manualResetEvent.WaitOne();
		Console.WriteLine("结束多线程...");
	}

	private static void printStr(string str)
	{
		Console.WriteLine("{0}是:{1}", str, Thread.CurrentThread.ManagedThreadId);
		// 设置为true
		manualResetEvent.Set();
	}
}

输出结果

启动多线程...
当前线程是:6
结束多线程...

ManualResetEvent类的参数值执行顺序如下:

  • false-- WaitOne等待      
  • true--WaitOne通过

注:一般情况下,不要阻塞线程池中的线程,因为写代码无意间造成的线程等待没有释放,一旦线程池线程耗尽就会形成死锁

造成死锁的案例: 设置了最大线程数是9,循环创建15个线程,意味着 后6个线程必然需要利用线程池中前面用过的线程, 但是由于前面创建的线程都直接让阻塞了,没有释放,这时循环到第10个线程时由于没有空闲线程,线程池没法执行就直接跳过了,主线程会继续执行后面的循环,这样也永远不会 执行 manualResetEvent.Set() 方法,最后就成死锁了

private static void Test1()
{
	// 设置最大线程
	ThreadPool.SetMaxThreads(9, 9);
	// 设置最小线程
	ThreadPool.SetMinThreads(3, 3);
	ManualResetEvent manualResetEvent = new ManualResetEvent(false);
	for (int i = 0; i < 15; i++)
	{
		int k = i;
		ThreadPool.QueueUserWorkItem(p =>
		{
			Console.WriteLine(k);
			if (k < 10)
			{
				manualResetEvent.WaitOne();
			}
			else
			{
				// 设为true
				manualResetEvent.Set();
			}
		});
	}
	if (manualResetEvent.WaitOne())
	{
		Console.WriteLine("没有死锁。。。。。。。。。");
	}
	Console.WriteLine("结束。。。。。。。。。。");
}

输出结果

1
0
2
3
4
5
6
7
8

更多**好看的内容**和**好玩的案例**请关注**我的微信公众号: 程序猿知秋**