目录

二 计算机术语

2.1 串行与并发

串行与并发是同一个维度的概念,区别是:

  • 串行:指令按照顺序执行

  • 并发:指令并未按照顺序执行,而是在宏观上同时执行,即CPU不停的在各个任务之间来回切换,给人感觉所有任务同时执行了!比如电脑同时运行了QQ、浏览器,其实是CPU在这2个程序之间按照一定的调度算法在来回切换执行!

并行与并发并不是同一个维度上的概念:

  • 并行(parallel):在同一时刻(微秒级),多条指令在多个处理器上同时执行,并行一般要借助多核CPU实现!

  • 并发(concurrency):并未同时执行,只是由于CPU运行过快,给人产生同时运行的假象

并发与并行概念的区别是是否同时执行,比如吃饭时,电话来了,需要停止吃饭去接电话,接完电话继续吃饭,这是并发执行,但是吃饭时电话来了,边吃边接是并行。

2.2 进程

进程:就是二进制可执行文件在计算机内存中的运行实例,可以简单理解为:一个.exe文件是个类,进程就是该类new出来的实例。
进程是操作系统资源分配的最小单位(如虚拟内存资源),所有代码都是在进程中执行的。

在Unix系统中,操作系统启动后将会运行进程号(PID)为1的一个进程 init 进程,该进程是所有其他进程的父进程。操作系统通过 fork() 函数能够创建多个子进程,从而能够提升计算机资源的利用率。

进程在创建后会拥有自己的独立地址空间,操作系统会提供一个数据结构PCB来描述该进程(Process Control Block,进程控制块),PCB中保存了进程的管理、控制信息等数据。

由于进程拥有互相独立的地址空间,所以进程之间无法直接通信,必须利用进程间通信(IPC,InterProcess Communication)方式来实现通信。

2.3 内核态与用户态

操作系统的内存会被划分为两大区域:

  • 内核区:提供了大量的系统调用函数,即最原生、最底层的操作函数,如 open(),write()

  • 用户区:加载、运行应用程序的区域,比如使用C语言写的程序,同样的C语言也提供了本语言的对应操作函数 fopen(),fwrite()。这些由编程语言提供的函数称之为库函数。

我们不难发现,库函数其实是在系统调用函数基础上再次进行了封装,方便开发者使用。当然开发者既可以使用库函数来操作文件,也可以直接使用底层的系统调用函数(但是这样需要做很多错误处理)。

程序在运行时,CPU有两种状态:

  • 用户态:当一个进程在执行用户自己的代码时处于用户运行态(用户态)

  • 内核态:当进程需要执行一些系统调用时,比如利用C的库函数fopen()时,fopen()虽然是库函数,但是执行时底层调用了系统的open()函数,此时程序进入内核态,调用结束后,程序会重新回到用户态!

操作系统之所以要这样设计是出于内存的安全考虑,内核地址只有内核自己的函数(系统调用函数)才能使用!

2.4 线程

线程:操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位(即cpu分配时间轮片的对象)

一个进程内部可以创建多个线程,他们与进程一样拥有独立的PCB,但是没有独立的地址空间,即线程之间共享了地址空间。这样也让线程之间无需IPC,直接就能通信!!(因为他们在同一个地址空间内)。

虽然线程带来了通信的便利,但是如果同一空间的中多个线程同时去修改同一个数据,就会造成资源竞争问题,这是计算机编程中最复杂的问题!

2.5 协程

进程和线程都是操作系统级别的,协程与他们并不是一个维度的概念,所以类似《现代操作系统》的书籍并未提出协程的概念。

贴士:千万不要将协程理解为轻量级线程!

协程:程序在执行时,函数内部可以中断,适当时候返回接着执行,即协程运行在用户态

协程的优势在于其轻量级、执行效率高:

  • 轻量级:没有线程开销,可以轻松创建上百万个协程而不会造成系统资源衰竭

  • 执行效率高:函数之间的切换不再是线程切换,由程序自身控制

线程需要上下文不停切换,而协程不会主动交出使用权,除非代码中主动要求切换,或者发生I/O,此时会切换到别的协程,这样能更好的解决并发问题。

三 并发理论基础

3.1 并发解决方案

  • 多进程:由系统内核管理并发,操作简单、进程互不影响。但是开销最大,占用资源较多,能开启的进程数极少,

  • 多线程:多线程在大部分系统上仍然属于系统层面的并发,开销较大,且会存在死锁管理问题。

  • 非阻塞I/O:基于回调的异步非阻塞I/O,尽可能少的运用线程

  • 协程:本质上仍然是用户态线程,但不需要系统进行抢占式调度,且真正的实现寄存于线程中,开销极小。

3.2 并发程序数据交互方式一:同步

线程同步:线程在发出某一个功能调用时,如果没有得到结果,则该调用不返回。此时其他线程不能调用该功能(因为要保证数据一致性)。

线程同步是为了避免引起数据混乱。实际上,多个控制流共同操作一个共享资源,都需要同步,比如:进程、线程、信号之间都需要同步机制,常见的线程同步技术就是互斥锁。

同步的作用是避免在并发访问共享资源时可能发生的冲突。

同步的理念:

  • 程序如果想使用一个共享数据,就必须先获取对它的使用权,当程序不再使用该资源时,则应放弃对该资源的访问权(即:释放资源)。

  • 资源的使用权被拿走后,其他访问该资源的程序不应该被中断,而是应该等到拥有使用权的程序释放资源之后再进行访问。
    即:在同一时刻,某个资源应该只被一个程序占用。

3.3 并发程序数据交互方式二:数据传递

除了使用同步方式来实现并发程序数据的交互之外,还可以使用数据传递方式(也称为通信)。

该方式可以使数据不加延迟的发送给数据接收方。即使数据接收方还没有为接收数据做好准备,也不会造成数据发送方的等待。数据会被临时存储在一个称谓通信缓存的数据结构中。通信缓存是一种特殊的数据结构,可以同时被多个程序使用,数据接收方可以在准备就绪之后按照数据存入通信缓存的顺序接收它们。

四 各个语言的并发理念

  • Java:典型的多线程并发模式,利用同步机制(加锁)来实现并发访问控制

  • Node.js:典型的单线程非阻塞I/O实践者,不存在Java的资源竞争问题,I/O操作处理完毕后才会利用事件机制通知业务线程返回结果,没有资源竞争的难题。

  • Go:典型的协程并发理念实践者,在语言本身层面实现了协程,协程之间通过管道进行数据传递

目前流行的并发理念是:异步非阻塞I/O,协程。