1、ThreadPool线程池
一个应用程序最多只能有一个线程池。线程池是一种多线程处理形式,通过QueueUserWorkItem()
将任务添加到队列中,然后创建线程(后台线程
,又称工作者线程)自动启动这些任务来处理。其中,最小线程数即核心线程数(corePoolSize)是线程池中长期保持的线程数,即使它们处于闲置状态,也不会被终止。普通线程空闲一段时间后,它们会被回收,以减少资源占用。
static void Main(string[] args){//获取默认线程池允许开辟的最大工作线程数和最大I/O异步线程数ThreadPool.GetMaxThreads(out int maxWorkThreadCount, out int maxIOThreadCount);Console.WriteLine($"maxWorkThreadCount:{maxWorkThreadCount},maxIOThreadCount:{maxIOThreadCount}");//获取默认线程池并发工作线程数和I/O异步线程数ThreadPool.GetMinThreads(out int minWorkThreadCount, out int minIOThreadCount); Console.WriteLine($"minWorkThreadCount:{minWorkThreadCount},minIOThreadCount:{minIOThreadCount}");//一般不为线程池中的线程数设置上限,因为将会导致死锁。//假设请求队列中所有的工作项全都因等待第1001个工作项发出信号而阻塞,如果设置了最大1000个线程,那所有线程都将阻塞var success = ThreadPool.SetMaxThreads(8, 8);//只能设置>=最小并发工作线程数和I/O线程数for (int i = 0; i < 20; i++){ThreadPool.QueueUserWorkItem(s =>{var workThreadId = Thread.CurrentThread.ManagedThreadId;var isBackground = Thread.CurrentThread.IsBackground;var isThreadPool = Thread.CurrentThread.IsThreadPoolThread;Console.WriteLine($"work is on thread {workThreadId}, Now time:{DateTime.Now.ToString("ss.ff")},"+ $" isBackground:{isBackground}, isThreadPool:{isThreadPool}");Thread.Sleep(5000);//模拟工作线程运行});}Console.ReadLine();}
运行结果:
适用场景:
- 不要将长时间运行的操作放进线程池中,适合
任务量大且短时
的场景,长时任务用Thread。 - 不应该阻塞线程池中的线程;
- 线程池最多管理线程数量=“处理器数 * 250”,最小不小于处理器数。
优点:
- 线程池能够有效地管理和复用线程资源,减少创建和销毁线程的开销,从而提高应用程序的性能和资源管理效率。
缺点:
- ThreadPool原生不支持对工作线程启动、取消、完成、失败通知等交互性操作,同样不支持获取函数返回值,灵活度不够,Thread原生有Abort (同样不推荐)、Join等可选择。
2、Thread
在System.Threading 命名空间下,包含了用于创建和控制线程的Thread 类。对线程的常用操作有:启动线程、终止线程、合并线程和让线程休眠等。
Thread默认是前台线程
。应用程序必须运行完所有的前台线程才可以退出;而对于后台线程,应用程序则可以不考虑其是否已经运行完毕而直接退出,所有的后台线程在应用程序退出时都会自动结束。
2.1、终止线程Abort
两种终止线程的方法:
- 一种是事先设置一个布尔变量,在其他线程中通过修改该变量的值作为传递给该线程是否需要终止的判断条件,而在该线程中循环判断该条件,以确定是否退出线程,这是结束线程的比较好的方法,实际编程中一般使用这种方法。
- 第二种方法是调用
t.Abort()
方法强行终止线程,Abort 方法没有任何参数,线程一旦被终止,就无法再重新启动。由于Abort 通过抛出异常强行终止结束线程,因此在实际编程中,应该尽量避免采用这种方法。
延申:
调用Abort 方法终止线程时,公共语言运行库(CLR)会引发ThreadAbortException 异常,程序员可以在线程中捕获ThreadAbortException 异常,然后在异常处理的Catch 块或者Finally块中作释放资源等代码处理工作;但是,线程中也可以不捕获ThreadAbortException 异常,而由系统自动进行释放资源等处理工作。
注意,如果线程中捕获了ThreadAbortException 异常,系统在finally 子句的结尾处会再次引发ThreadAbortException 异常,如果没有finally 子句,则会在Catch 子句的结尾处再次引发该异常。为了避免再次引发异常,可以在finally 子句的结尾处或者Catch 子句的结尾处调用System.Threading.Thread.ResetAbort 方法防止系统再次引发该异常。
使用Abort 方法终止线程,调用Abort 方法后,线程不一定会立即结束。这是因为系统在结束线程前要进行代码清理等工作,这种机制可以使线程的终止比较安全,但清理代码需要一定的时间,而我们并不知道这个工作将需要多长时间。因此,调用了线程的Abort 方法后,如果系统自动清理代码的工作没有结束,可能会出现类似死机一样的假象。为了解决这个问题,可以在主线程中调用子线程对象的Join 方法,并在Join 方法中指定主线程等待子线程结束的等待时间。
2.2、合并线程Join
用于把两个并行执行的线程合并为一个单个的线程。
如果一个线程t1 在执行的过程中需要等待另一个线程t2 结束后才能继续执行,可以在t1 的程序模块中调用t2 的join()方法。
例如:t2.Join()
;这样t1 在执行到t2.Join()语句后就会处于阻塞状态,直到t2 结束后才会继续执行。
但是假如t2 一直不结束,那么等待就没有意义了。为了解决这个问题,可以在调用t2 的Join 方法的时候指定一个等待时间。
例如:t2.Join(100)
;将t2 合并到t1 后,t1 只等待100 毫秒,然后不论t2 是否结束,t1 都继续执行。
Join 方法通常和Abort 一起使用。由于调用某个线程的Abort 方法后,我们无法确定系统清理代码的工作什么时候才能结束,因此如果希望主线程调用了子线程的Abort 方法后,主线程不必一直等待,可以调用子线程的Join 方法将子线程连接到主线程中,并在连接方法中指定一个最大等待时间,这样就能使主线程继续执行了。
2.3、线程优先级
当线程之间争夺CPU 时间片时,CPU 是按照线程的优先级进行服务的。在C#应用程序中,可以对线程设定五个不同的优先级,由高到低分别是Highest、AboveNormal、Normal、BelowNormal 和Lowest。在创建线程时如果不指定其优先级,则系统默认为Normal。
Thread t=new Thread(new ThreadStart(enterpoint)); t.priority=ThreadPriority.AboveNormal;
通过设置线程的优先级可以改变线程的执行顺序,所设置的优先级仅仅适用于这些线程所属的进程。
注意:当把某线程的优先级设置为Highest 时,系统上正在运行的其他线程都会终止,所以使用这个优先级别时要特别小心。
3、Task
Task是.NET Framework 4.5加入的概念,之前实现多线程是利用Thread类,在实际编码中基本用Task,因为它比Thread更易理解,更易运用,更安全可靠。
Task是基于线程池封装实现的,解决了线程池无法挂起中止线程等这些问题。而且Task的性能优于ThreadPool因为它使用的不是线程池的全局队列,而是使用的是本地队列。使得线程之间竞争资源的情况减少。Task提供了丰富的API,开发者可对Task进行多种管理。
Task和Thread差异:
- task与thread对比,task相当于应用层,thread更底层,但二者是不一样的,没有隶属关系
- task是在线程池上创建,是后台线程(主线程不会等其完成);Thread是单个线程,默认是前台线程
- task可以直接获取返回值,thread不能直接从方法返回结果(可以使用变量来获取结果)
- 使用task.ContinueWith()方法可以继续执行其他任务。Thread无连续性,当线程完成工作时,不能告诉线程开始其他操作。 尽管可以使用Join()等待线程完成,但是这会阻塞主线程
- task借助CancellationTokeSource类可以支持任务中的取消,当thread处于运行中时,无法取消它
- task能方便捕捉到运行中的异常,thread在父方法中无法捕捉到异常
task的使用