并发是计算机编程中非常重要的一个概念,其中涉及了许多高级技巧和模式,对于一个优秀的程序猿来说掌握好并发编程是一门必修课,接下来的几篇文章我将就我自己对并发编程的一些看法和实践经验对这方面进行一下梳理和总结,欢迎各路大神批评指正,大家在交流中共同进步~~
- 并发是什么?
并发是指多任务的同时执行,从而使现有资源得到充分利用并节省时间。并发是一种思维方式,并不限于特定领域,对于编程来说也不限于特定的编程语言。即使在我们的日常生活中,并发的例子也比比皆是。比如我们可以在烧水的时候看书,还有我们经常做的在马桶上玩手机……
为什么要使用并发编程?
- 资源利用率:从整个程序的执行角度来看,程序执行时可以看作是对输入的数据进行计算处理然后输出到特定的设备中。如果这条流程线完全是串行执行的话,当其中的一个环节正在执行的时候其他环节就不能工作。这就意味着一旦输入阻塞,即IO等待读入数据那么已读入的数据也不能得到处理,已处理的数据也不能输出,这就造成了CPU的闲置。而如果这三个步骤可以并发执行的话即使IO在等待输入CPU仍然可以对已在内存中的数据做计算处理,结果也可以正常输出。这就提高了CPU的利用率,不会因为输入输出的阻塞导致CPU的计算能力被浪费。
- 时间:很多任务彼此之间并没有什么关联,当有充分的资源可以使用时,它们可以同时被执行,与串行地执行任务相比,这样既可以充分利用现有资源,提高资源的利用率,同时又可以减少任务完成的总时间,可以节省出更多的时间处理接下来的任务。
- 公平性:对于同优先级的任务来说,它们应该能够受到计算机资源的同等待遇。如果是串行执行的话就意味着有先有后,这就造成了任务处理的不公平性,并发就可以很好地解决这个问题,它们或是同时在不同的CPU上执行,或是在单个CPU上交替执行,保证了任务应该享有的公平性。
- 简便性:当有多种类型的任务执行时,为每种任务单独编写程序比编写混杂在一起的所有任务的处理程序要简单的多。试想当我们在处理多种事情时,把每种任务都分配给单独的人员比起把每种任务都平均分配然后让相应人员都处理所有种类的任务相比,效率肯定要高的多。一方面是因为一直做一件事会做的越来越熟,更重要的是可以专心做一件事而不用受到其他事情的干扰,这一点想必已经工作的朋友一定深有体会。
并发编程要靠什么实现?
现在来讲,并发编程的实现方式一种是多进程的并发,一种是多线程的并发。从操作系统的角度来看,进程是资源分配的基本单位,线程是任务调度的基本单位,线程是轻量级的进程但它不能脱离进程存在,也就是说线程使用的资源都是从宿主进程获得的。由于特定语言的语言特性和相应解释器的执行原理,并不能笼统地说哪种方式更好。比如说在java中我们说的并发编程一般就是指多线程编程,而在python中可能就会取决于IO密集型还是CPU密集型在两种并发方式之间进行权衡。以下文章中的讲解我将会以java中并发编程为例,所以之后所说的并发编程默认就是多线程并发编程。并发编程有什么风险吗?
坦率地说并发编程相对于一般的串行编程来说是存在很大的风险的,如果把我们之前在单线程中运行的程序不加改造地拿到多线程中去,很有可能是不会有正确结果的。 导致结果不正确的原因就是并发编程中我们需要格外注意的地方:- 安全性:安全性的核心是正确性,它是要能保证并发编程不会出现不符合预期结果的情况。比如说我交学费的时候只能有两种情况:钱从我卡里进了学校的账号,学校的缴费系统也显示我已经交了学费;我没有交费或者交费失败的时候钱还在我卡里,学校的缴费系统显示我还没有交学费。一旦出现我交了钱系统却显示我还没有交费,我交费失败系统却显示我已交费的情况(当然我个人还是蛮喜欢这种情况的~~)都是违背了安全性的原则。
- 活跃性:与安全性的定义差不多相反,它要保证并发编程中正确的事情一定会发生。比如说程序一旦陷入死循环,循环后的代码不能被执行,或者多线程间的死锁导致程序假死都没有达到活跃性的要求。
- 开销:并发编程并不是”空手套白狼”,它也是有开销的。不论是多进程还是多线程的并发,在他们切换的时候会执行上下文的切换,寄存器中变量的更新等等操作,这都会消耗一定的资源和时间,所以并发编程一定要预估好并发编程节省的资源和时间是不是足以弥补线程间切换的开销
- 复杂性:单线程中所有变量的值都可以从本线程对应的栈、寄存器和进程公共的堆拿到,执行时也是孤军奋战,不需要和别人打交道,而多线程之间一旦需要协同就要在线程之间传递信息,再加上前面所说的对安全性、活跃性等的要求,这一整套多线程的并发机制会大大提高程序的复杂性,这也是并发编程的难度所在。
什么时候适合使用并发编程呢?
任何事情的优势就决定了它在什么情况下适用。正如上面”为什么要使用并发编程”中所说,并发编程最大的优势就是提高程序的运行速度和资源利用率,而如果串行执行的程序在这两方面并不受到限制的话就没有必要使用并发编程了,而如果在这两方面遇到了瓶颈需要有所突破或者你就是个该死的极致性能追求者的话,就要考虑你的程序场景是不是符合下面这些并发编程的用武之地了:- 任务会阻塞线程,导致之后的代码不能执行:比如一边从文件中读取,一边进行大量计算的情况
- 任务执行时间过长,可以划分为分工明确的子任务:比如分段下载
- 任务间断性执行:比如上报crash,日志打印
- 任务本身需要协作执行:比如生产者消费者问题
- ……
在实际的并发编程时都需要注意什么呢?
上面所说的并发编程的风险大概就是并发编程的注意点,更具体一点,从并发的编程角度来看,我个人更愿意将并发编程分为三部分:- 多线程的并发执行:这是并发编程的核心,研究的是如何保证任务在不同线程中并发执行从而提高程序的运行速度
- 线程间的通信:线程的执行虽然是并发的,但是他们所执行的任务并不一定是独立的,它研究的就是如何实现任务所在线程之间的高效、可靠的通信
线程间对共享状态的同步与互斥:线程之间会共享一些对象,我们称之为状态,当多线程同时读写某个共享状态时可能会因不恰当的执行时序而造成程序逻辑的混乱,如何保证共享状态的互斥(即保证任意时刻某个共享状态只能由单个线程访问)和同步(当前线程的值都是上一线程执行完后的最新的值)
之后的文章也是围绕这三方面来展开的,其中多线程的并发执行是基础,毕竟如果没有并发,也就谈不上线程间通信和共享状态了。然而就多线程并发执行单方面仅仅是解决性能的问题,而如果没有线程间通信和对共享状态的保护,恐怕连最最基本的正确性都不能保证了。因此,这三方面对于并发编程来说缺一不可,任何一项的短板都不能让我们成功编写优秀的并发程序。