[JCIP笔记] (一)多线程的起源
在很久很久以前,那时的计算机还没有操作系统这种东西,所以只能有一个程序,从头到尾地跑。于是这个程序要负责使用所有的资源,还得响应外部请求。想想这个程序得多复杂啊——为了做成一件事,可能要先把内存啊、IO啊这些外缘的东西搞定,而且在做事的时候还不能及时响应用户的请求。同时,由于这个程序一次只能做同一件事情,在某种程度上相当于浪费了宝贵的计算机资源。
于是,大神们发明了操作系统。在操作系统中,许多进程可以同时执行,操作系统给它们提供可以共享的资源——内存,file handle,以及安全凭证等。这样就可以提升运行的效率了,因为不同进程可以各自去做一些独立的事情,访问相关的系统资源;同时,由于可以使用不同的进程去解决不同的问题,也给程序员编程带来了便利。
在早期的操作系统中,每个进程都是一个虚拟的冯·诺依曼结构,程序中的指令是被顺序执行的,也就是说程序的控制流在程序执行之前就是确定的。这个串行执行的模型听起来非常简单,但是过了一段时间以后人们不禁开始思考:这是最好的做法吗?
在人类的日常生活中,经常需要“同时”去处理许多事情,比如做早饭时,把壶架在火上开始烧水,同时把吐司放进烤面包机,然后开始刷牙、洗脸,直到水烧开、面包烤好……人类很聪明,不会站在那里等待水烧开才去烤面包,因为这样做不仅水会凉,更主要的是会浪费时间。把水壶放在灶上,利用烧水的时间去做别的事,而水烧开时,水壶会自动发出声音提醒你水已经烧好了,这个过程在计算机的消息通信机制中有个术语,叫做“异步”。相反,一直站在那里等水烧开了才去做下一步的事情,叫做“同步”。高效的程序需要在串行和异步之间寻找一个平衡。
基于这种“同时做好几件事”的考虑,大神们把进程的发明更进一步,创造了线程。同一个进程中的线程可以共享内存和file handle等资源,但每个线程还有自己的program counter, 栈和局部变量等。这就相当于把进程带来的并行性又增强到一个更细的粒度。多线程的一个显著的优势在于高效的资源利用——在许多操作系统中,线程是最小的调度单位。多个线程可以同时被分配到多个CPU上被执行。几乎可以说,在多核系统上,不使用多线程是对系统资源的一种浪费。
除此之外,如果一个程序中有大量的不同性质的任务要做,我们可以把他们划分到不同的线程中进行处理,使它们成为各自独立的任务,这样大大降低了编程的难度。多线程的存在也大大提升了程序UI的响应度。在UI系统中,设置一个分发线程去听取用户的请求,收到请求后交给其他线程去做复杂的操作和处理,比用单线程进行处理好很多。如果在单线程UI中进行复杂的长时间操作,会让整个UI失去反应,而且无法取消当前的操作。
当然,多线程也为程序员带来了许多挑战。其中最大的挑战是,程序员需要努力不使自己的程序出现线程安全问题。 拿Java举例,如果多个线程同时访问某个数据,且访问时没有足够的同步机制,会导致一种叫做race condition的现象,即程序的运行结果与多线程被调度的时间先后顺序有关,这是我们不希望出现的——我们希望在前置条件一定的情况下,每次运行的结果都是相同的。比如单例模式中,某个类必须最多只有一个对象产生。
另外,多线程程序比单线程程序容易出现liveness问题,比如死锁。多线程的调度和上下文切换也是有代价的,会造成一些额外的性能开销。那么我们在什么情况下才去使用多线程,怎么去规避这些问题呢?这就是本书作者们会贯彻全书给我们讲解的问题。