C#夯实基础之多线程一:初识多线程

一. 烧水沏茶问题

      在小学四年级有一个烧水沏茶问题,可以作为我们今天讨论话题的引子:

      客人来了,要烧一壶茶,但是烧水需要5分钟,洗水壶需要1分钟,洗茶杯需要2分钟,接水需要1分钟,找茶叶需要1分钟,沏茶需要1分钟,问如何使客人能最快地喝上茶?

      这是一个简单的时间安排问题,我们来分析一下.

一杯茶需要有什么?杯子,热水,茶叶,这三者都齐了就可以沏茶了,那集齐三者需要哪些步骤?见下图.

        

  如上图所示,烧水最麻烦,需要3个步骤,其中带有人头像的都是需要人参与的.其中烧水的过程是不需要人员参与的,我们可以利用烧水这段时间去做其他的事情.如果以时间轴来描述这件事情,那就如下所示:

    

    答案显而异见,因为我们利用烧水的时间做了另外两件事,节省了时间,所以,最短时间就是8分钟.

    以上这个简单的案例可以推广到生活中的方方面面,同时,它也有一个高大上的名字——统筹规划,不过这不是我们这里讨论的主题,按下不表。

    既然我们是讲多线程,那让我们把这个案例类比到日常发邮件的问题。

  

 

  我们对比一下两个案例,会发现,二者的案例都是以人的动作作为时间轴往前推进的,但唯一不同的是,前者的处理中心是人的大脑,后者的处理中心是电脑的CPU

 二.为什么是多线程?

      人类的进步是在发现问题和解决问题的无休止循环中实现的.计算机即是一个典型的例子.

       

         计算机刚开始的时候,是顺序执行的,所有的任务都系于一命,举个例子,当你在使用打印软件打印文档的时候,你只能等计算机完成打印,而不能再输入,也不能另外打开浏览器,如果打印机有1000个文档在排队,虽然打印机是自动打印的,但你可能需要等上一天才可以继续使用计算机,这就浪费了大量的等待时间,这还不是最坏的,最坏的是”串数据”,打印机的数据有可能被其他的程序使用并删除,这时的计算机并不能同时做这么多事(一心多用),而是一段时间只能做一件事(一心一用).

   进而,windows使用多进程来解决这种问题,进程作为资源分配的最小单位,每个应用程序使用的都是隔离开的资源,不会造成”串数据”的危险.但像前面打印机等待的问题依然存在,因为资源虽然是分开了,CPU仍然只有一个,如果一个程序占用CPU不撒手,则有可能造成其他的应用程序都使用不了CPU进而无法运行,这样的问题怎么办?

       Windows使用线程来解决这个问题,既然多进程的症结在于CPU,线程是对CPU的虚拟,CPU的使用按时间片划分,每次只能执行一个线程,使用完,切换上下文,执行等待队列的其他线程,这样使得多个任务同时进行,使得我们”好像”拥有多个CPU,这样哪怕一个应用程序无法响应,也不影响其他应用程序的运行,也可以通过更高优先级的”资源管理器”来抢占CPU进而终止无响应的线程.

   仔细想想,多线程的思想跟我们烧水沏茶(发邮件)的思路是不是有非常类似的地方,我们把每1分钟当作一个CPU时间片,在烧水(上传附件)的时候,我们使用了另外一个工具烧水器(上传线程),把它作为人能力(CPU处理能力)的扩展.当然,这里,使用工具扩展人的能力是客观存在的,也就是说确实是工具在帮助人类做事,CPU只是因为处理速度极快,他不断地切换时间片来处理多个任务,在外部看起来,好像多个任务同时被处理,正如电影每秒24帧看起来非常流畅没有中断一样.从表面上来看,这使得人类(电脑)拥有了同时处理多个事务的能力.

   那windows给我们提供了怎样的多线程?它通过CLR暴露出来给我们的接口有哪些?这些具像的问题,就不是我们这篇文章所讨论的了.

 三.多线程面临的基本问题

      我们知道,windows发展到现在,最终选择了多线程,但是多线程也不是最终的解决方案,使用它仍然会出一些问题,而在没有发展出更好的解决方案之前,防止这些问题的终极解决方案就是各位程序员写出高质量的多线程程序.

      那么,多线程面临着什么基本问题?

     1.资源争用(资源同步)

    我们知道,进程是分配资源的最小单位,线程是CPU调度的最小单位.也就是说,多线程中,多个线程共享进程的资源,举一个适合吃货的例子:大家在一个桌子上吃饭,共用一个汤勺,如果你想喝汤,我也想喝汤,那么,谁先谁后?桌子上的菜和餐具都是资源,汤勺也是资源.

       也许,有的同学说,这个好办,先来后到,谁先拿了汤勺就谁喝汤,剩下的人员排队就好了.,这是一个好办法,也是资源争用的一个基本解决思路.

       现实生活,我们知道的概念,但程序不知道如何,我们用锁机制来实现的机制,当我们拿到汤勺的时候,我们告诉其他人,”这个汤勺我目前占用了”,那别人就知道要等我用完了,再来取汤勺.

    2.死锁

   锁机制又会带新的问题,这里假设,桌子上的汤碗不够,需要多人共用汤碗,这时,张三拿着汤勺等李四的汤碗,李四拿着汤碗等着张三的汤勺,双方都占着这些东西不撒手,陷入无限等待的循环,这就造成了谁也喝不了汤的窘境.而这,也就是锁机制带来的问题——死锁.

       死锁怎么解决?只有靠程序员,这就是我们今天讨论多线程的意义所在。

       总的来说,多线程带来的基本问题就是资源争用,我们用锁机制来解决资源争用的问题,但又会带来死锁的可能。因而,在使用多线程的时候,我们要用锁,但要防止死锁。

   其实,多线程的细节问题(非基本问题)远不止这些,下面列举一二:

      ◎前面我们在讲到烧水沏茶的时候,如果烧水、沏茶、洗杯子、找茶叶,4个人分别在做,那沏茶的人需要等待其他3个人都完成后才能沏茶,他如何得知3个人都完成了任务呢?那3个人是如何通知他的呢?

      ◎如果客人突然要走了,那么如何取消这个沏茶行动?

      ◎前面我们说到汤勺问题的时候,如果我想得到汤勺,如果一段时间得不到,是不是我就要非喝这个汤不可呢?我可不可以放弃,换个面吃吃?

      ◎前面汤勺问题中,如果我们是两个桌子(进程)共同一个汤勺,那么两个桌子如何沟通汤勺的问题?如何占用汤勺?

      像这样的问题在多线程中是非常多的,前面我们说过,锁可以用来解决资源争用的问题,而像这些各种情形下的争用问题,.NetFramework的解决方案就是各式各样的锁配合各种线程类。比如,我们可以使用互斥锁来实现多线程同步的问题,可以使用超时、等待机制来避免无限等待,可以使用通知机制来实现多任务的合并,可以使用取消机制来实现线程的取消,可以使用父子任务来实现任务的层次关系,可以使用进程互斥锁来实现进程间的同步.而这些都是我们需要在多线程中需要了解并掌握的.

四.推荐两篇文章

     pansz在知乎的回答 https://www.zhihu.com/question/19901763    

     阮一峰 http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html

    

 

posted @ 2016-12-26 14:36  gudi  阅读(833)  评论(0编辑  收藏  举报