多线程的那些事儿(2)-- 进程与线程的区别与联系

  在进行多线程编程之前我们先解决一个基本问题:什么是线程、什么是进程,他们之间有什么区别与联系。

  

(1)进程:运行环境

                  线程:执行单位

        用书面一点的知说,进程是一个计算机中程序运行的一个实体,线程是操作系统能够进行运算调度的最小单位。一个运行着的程序就是一个进程,一个进程中至少有一个线程正在运行,而实际上进程只是个容器,而本身不具有任何CPU的调度功能,当然也无从谈起如何执行。进程为线程提供运行的环境,而进程本身只是个抽象的东西,不能正常运行。

(2)同一进程中的多个线程

        一个进程也可以包括一个以上的线程,这些线程都在这个进程的同一个运行环境中运行,包括地址空间、内核对象(包括信号量、临界区、文件句柄、Socket等),而从本质上讲,就是同一个进程的不同线程共享同一个地址空间,所谓的内核对象可以共享是因为这些对象被映射到了这个进程的地址空间中。

        进程与线程的关系可以通过一个比喻来理解,进程好比就是一个公司,而线程就好比这个公司里的人。如果要创建一个公司(进程)就必须至少包含一个人,这个人就是法人(主线程),而线程的多少从理论上讲是没有上限的,但这个公司的资金(内存和CPU)是有上限的,不是人越多越好,人越多消耗的资金也就越多,如果入不敷出就会导致这个公司的破产(崩溃)。上一文说到线程的个数也一样的道理,一个公司的不同岗位需要的人的多少首先与公司的业务相关联,再就跟公司的资金和硬件条件也有关系,不是越多越少,也不是越少越好,而要通过实践总结出一个合适的人数。

(3)堆与栈

        进程中的内存空间除了代码、静态存储区还有就是动态存储区,这些区域包括两大块:堆(Heap)、栈(Stack)。堆是用于程序进行动态分配时的内存区域,而栈是编译器静态分配的区域。就是说,堆上到底存什么,只有在程序运行时才能确定,是程序员自己管理的区域,而栈则是编译时编译器就已经分配好的存储空间。堆就是用来存放那些用new(C中用calloc, malloc)创建的对象,而栈用于存入函数调用的现场保护、局部变量。由于堆对于各个线程来就是用来存放普通的对象,对于每个线程都是一样的,所以堆可以是全局的,就是说所有线程可以共用一个堆,而栈则不可以,每个线程都有独立的栈。因为栈内不仅可以存普通数据,还需要存储函数调用时的现场保护、参数传递,它是有顺序的,而堆是无序的。如果一个进程中只有一个线程,这种情况是最简单的,除了固定的代码和静态数据区,其余的空间可以分配给堆和栈,一个自顶向下增长,一个自底向上增长,只有当二者接上头的时候才会是内存用光的时候。多个线程的时候情况就比较复杂,每个线程在一开始创建时就必须确定栈的位置和大小,因为栈在同一个地址空间,如果不确定最大值一直涨会导致两个线程的栈重叠,那时程序将无法运行,到底线程的栈是多少由程序员而定,前提是不要超过进程的空间大小和物理内存的大小。

        *所以,在进行多线程编程时一定要留心你在栈上创建的对你的多少,比如用C语言创建局部变量数组时,如果数组太大就会造成栈溢出,程序就会崩溃。大块的内存使用要在堆上,或定义成全局变量存放在静态数据区。

(4)线程池

         对于多线程编程,线程池是常用技术之一,这就好比一个公司同一个工种的一个工作组,比方说这个组是客服。随时都有可能有客户打电话进来,具体是谁去接这些电话呢,当然是谁空闲谁接,如果都忙的话,再打进来只能排除,真到有一个客户服务人员空闲下来。为什么要用线程池呢?试想,我们不知道到底同时有多少个客户会打电话进来,从理论上讲可以这样,没有电话打进来的时候一个客服都不招,当有电话打进来时再招一下客服,等电话接完了再把他给辞掉,再有电话进来再招、再辞。当然这样同样可以完成工作,但是招一个客服人员、辞掉一个客服人员都需要招聘、面试、入职、离职等手续,成本是很高的。与创建、销毁线程的道理是一样的,创建一个线程、销毁一个线程好比招一个客户服务人员再辞掉一样成本很高,所以先创建几个线程,有电话就接进来,谁空闲谁接,没有电话的时候就暂时都休息一下。到底招多少个客服合适呢,这不仅跟内存(资金)有关系,还跟CPU(电话)的多少有关系。如果只有3部电话,招4个客服,肯定会有一个没有位置接电话,如果硬是要让这4个人在3部电话前不断换来换去,反而不如3个客服效率更高,因为轮换客服人员(切换CPU)是要浪费电话的利用时间的,所以影响招客服人员多少的三个重要因素就是:业务、资金(内存)、电话台数(CPU数量)。

(5)线程同步

        多线程编程时,最容易出现的一个问题就是数据竞争。比如有两个线程,一个线程T1从外部读取数据放到一块内存中,读取完成后由另外一个线程T2进行处理。如果T2在处理的过程中,刚读取了一半,而T1又往数据区写入新的数据,这样势必会造成T2处理的数据其实是上一次的一部分和下一次的一部分,处理的结果肯定是错的,这就是两个线程产生了数据竞争。要解决这些问题就必须通过线程同步时间,涉及到使用临界区、信号量、互斥量等一些线程同步方法,使用不同语言都会有类似的API,稍后再讨论。当线程与锁上来之后,又会带来负作用,那就是“死锁”,稍后做详细的讨论。

(6)前台线程和后台线程

        在Java和C#中都有前台线程的后台线程之分 ,当所有前台线程都终止进程才会终止,只要有一个前台线程,进程就不会结束,而后台线程则不影响进程的结束,不论有多少个后台进程的运行,只要有一个前台进程在运行进程就不会结束。默认都是前台线程,这些线程应该都是主要线程,只要有一个退出程序就应该退出,如果其它线程结束后这个线程无论如何也必须结束,则这个线程应该设置为后台线程。

(7)线程内异常

       记得在所有线程内正确处理异常,否则只要有一个线程内出现异常,整个进程就会崩溃,所以在线程的过程函数的最外层捕获所有异常,即使你不能正确处理也要记录下来现场,知道程序崩溃的时候发生了什么,便于找出问题所在修复问题。

        

      


发布了311 篇原创文章 · 获赞 225 · 访问量 84万+

猜你喜欢

转载自blog.csdn.net/Nocky/article/details/8498234
今日推荐