【并发编程】对阻塞/非阻塞、同步/异步、并发/并行等基本概念的理解
1. 并发与并行
并发:concurrency 并行:parallelism
开发过程中,常常会接触并发有关的概念,比如并发计算(concurrent computing),并发系统( concurrent system),并发控制(concurrent control),并发编程(concurrent programming),那么并发到底是什么呢?
可以简单的理解为:同时做多件事情的能力!
一个人边走、边唱、边想,这就是并发;反之,要是先走、再唱、再想,就是顺序(sequentially )执行,不是并发。再比如 Web 服务器,如 Apache,nginx,能够同时处理多个客户端连接,就是并发处理。
并发与并行是两个相关(related)但不同(distinct)的概念,都是“同时做多件事”,但并发关注的点是多件事被同时(一段时期)做,但只有一件事情正在执行(单核)!而并行关注的点是多个“人”在同时做事情。比如单核系统(time-sharing)能实现并发处理,但只有多核系统才能实现并行处理。
2. 阻塞与非阻塞
阻塞就是等待任务结束,在此期间,什么也不干!非阻塞就是不等待任务结束,继续做后面的事!
比如,快递员把快递送到小区门口,打电话给客户,等他到小区门口把快递拿走,再送下一件快递,这就是阻塞模式;快递员把快递放在小区的快递箱中,给客户发了短信,就送其它快递了,这就是非阻塞模式。
常见的阻塞形式有,网络阻塞,磁盘读写阻塞等。
3. 同步与异步
同步:synchronous 异步:asynchronous
同步是什么?为什么要同步?
同步就是让多个任务在某一阶段顺序执行,是并发控制的一种方式,目的是保证数据的正确性,完整性,一致性!比如数据库中的事务机制,并发编程中的锁,信号量,条件变量等,都是实现同步的工具。
只有在并发系统中,涉及到共享数据的访问时,才需要考虑同步问题!但是实际开发中,常常会遇到一些非必要的同步,比如 write 100M 的数据,或者建立数据连接,都是“去同步化”的目标,就需要考虑使用异步编程了。
异步就是让顺序执行的任务并发执行,那些根本不涉及共享数据访问、逻辑独立的任务,异步执行更高效!
4. 并发编程的实现方式
1 系统层面: 进程(Process),线程(Thread)
现代操作系统 Unix/Linux/Windows 都是支持“多任务”的操作系统,为并发编程提供了不同的任务模型——进程,线程,程序员可以借助多线程+多进程来实现并发编程。
线程是最小的执行单元,而进程由至少一个线程组成,线程/进程调度,如何时执行,执行多久,由操作系统来决定。数据共享和并发控制是多线程/多进程编程时面临的重要挑战,增加了编程的复杂度;另外,频繁的上下文切换,也会影响 CPU 的使用效率。
为了减少线程切换,会使用线程池来管理线程。
2 语言层面: Channel,Coroutine,Futures and Promises
并发编程的灵活度会极大的影响一门编程语言的表现力,比如 Python 中的协程、async/wait,C# 中的 Task,async/wait,Go 中的 Channel。
另外 future, promise, delay, deferred 也是编程语言中并发编程模型相关的术语。
5. 总结
首先,好的程序应该能充分利用有限的 CPU 资源,努力提高资源利用率,该需求可以通过多线程/多进程来实现。
可是,线程/进程是稀缺资源,数量有限,且线程/进程上下文切换成本高。程序中的线程越多,上下文切换的成本越高,CPU 有效利用率越低!另外,在多线程/多进程模型中,需要考虑并发控制的问题,增加了编程的复杂度。
那么如何实现不依赖多线程/多进程的并发编程呢?此时,异步编程就应运而生了!
一方面,异步/非租塞通过减少对线程/进程的依赖,提高了 CPU 资源的有效利用率,同时避免了多线程/多进程中并发控制的问题;另一方面,异步编程可以降低编程的复杂度。