自以为是的多线程(一)
多线程在web开发里面其实应用场景并不多,而且应用到多线程的场景也大多都是一些比较简单的场景,基本上大多都可以用Task代替,所以很多web开发人员对多线程的理解非常的浅薄,也就导致了会出现很多不可预计的bug,然后又因此写了一大堆逻辑来绕来绕去,所以我想谈谈多线程,试图做到高屋建瓴,给大家一个比较开阔的视野。
这一篇先从性能角度来说,下一篇从线程安全角度来讲解。
先解释一下,为什么线程不是越多越好:
谈到多线程必须要谈到cpu,一个单核心cpu在同一个时间内,只能执行一个线程(受限于目前技术),对于超线程cpu(intel专利,使用了超线程技术的cpu,容许一个核同时执行两个线程,在windows操作系统里面也会显示有两个核心),我们都理解成是双核心cpu。
而对于每个线程,都一定包含以下几个要素:
1、线程内核对象,系统为每个线程创建的,包括上下文(context)等。
2、线程环境块,是一块内存,包含线程的异常处理链首。
3、用户模式栈、内核模式栈。
4、DLL线程链接和线程分离通知
这个是我们在创建一个新线程时,系统在为线程初始化时需要创建的东西。
而前面又提到了,一个单核CPU同一时间内只能跑一个线程,那么当一个CPU正在跑一个线程的时候,这个线程的很多数据已经存到了CPU的高速缓存中,这个时候发生了切换,我们需要切换到另外一个线程上面去执行,那这个线程执行的可能是另外的代码,需要读取另外的数据,这个时候CPU就需要重新去内存里面去取数据,来填充这个高速缓存。而CPU去内存里面取数据的这个过程,相比于去高速缓存里面取数据来说,是很慢的,这也就是说,当我们频繁切换线程的话,CPU就需要做很多额外的事情。这也就是说,当只有单核CPU时,同样的功能,一个线程肯定比多个线程更快。这里有一个矛盾,就是系统不可能只有一个线程,系统还牵涉到很多自己的系统线程,以及其它的应用程序的线程,那系统在做线程切换(系统决定调用哪个线程)的时候,会牵涉到一个线程优先级的问题,而这个调度方法(相对合理智能的一个算法)是我们不可控的,所以说当你的程序有多个线程的情况下,在做线程切换的时候,切换到你的线程上面的概率会变大,所以也不能绝对的说一个线程就一定比多个线程快,这里只是一个大概的相对理论情况。
既然是这种情况,那我们在写代码的时候,就应该尽可能的避免出现线程切换,而让CPU尽可能的执行该线程。可是在什么情况下会容易出现线程切换(以下所有的线程切换都是指的相对或是可能,因为线程调度不可控)。
比如我有以下代码:
public static string ReadText(string path) { string text = ""; if (File.Exists(path)) { using (Stream fs = File.Open(path, FileMode.Open)) { using (StreamReader sr = new StreamReader(fs)) { text = sr.ReadToEnd(); sr.Close(); } } } return text; }
这个是很常见的IO读取,这个时候会给IO线程发出一个请求,然后等待IO的响应,在等待的过程中,系统会把这个线程锁定(这是个很棒的设计),让CPU去做其它事情,等到执行响应完毕以后,再唤醒该线程。同理,在做数据库的读取以及一些其他的IO请求时,都会这样。假如当有一个用户请求时,我们就会执行这样一段代码,那当有不断多的用户请求时,系统就会创建不断多的线程(创建线程的本身开销就很昂贵),而当IO读取完响应的时候,又会有不断多的线程逐渐被唤醒,系统这个时候就又会疲于线程切换,你就会发现性能开始巨降。
同理,当我有以下代码时:
lock(object){ ... }
当多个线程在执行这段代码的时候,就会出现很有意思的情况。
假如1-10,一共有10个线程需要执行到这段代码,假如cpu是2个(分别为A、B,分别执行的是1、2),当1执行的时候,2被锁定,这个时候B CPU就开始做线程切换,调度3-10中的任意一个继续执行,如果这段代码较长,A不一定能在短时间内执行完,那就会出现,调度一个锁一个,继续切换,再调度,再锁定,再切换的一个循环,一直到所有线程都被锁定为止。
当1执行完毕的时候,这个时候唤醒所有被锁定的线程,然后重新分配给A、B两个CPU,然后当A又执行到这里的时候,B又会出现刚刚再调度、再锁定,再切换这样的一个循环情况。所以在多线程开发的时候,尽可能的避免共享资源的出现。
ps:那当我们在做多线程开发的时候,什么时候用线程池、什么时候自己写线程。Task也可以理解为线程池。
线程池的优势就是,当逻辑执行完毕以后,并不销毁线程,而是将线程挂起,当你需要使用线程的时候,就会给你分配一个空闲的线程去执行你的逻辑,让线程反复使用,因为前面有提到了初始化线程需要创建很多对象,开销很昂贵。只有当所有的线程都处于繁忙状态时,没有线程分配时才去给你重新创建一个新线程。
可是如果你的程序在某一个时间段有一个峰值的话,那么最繁忙的时候,程序就会创建N多个线程,而当峰值过去了以后,这些被创建的线程不会被释放掉,会一直占用这你的资源。一直等到GC,才有可能被释放掉。
所以各位看客根据自己的业务情况来决定,是否使用线程池。