一. 并发并行
并发:系统具有同一时间段内处理多个任务的能力。
并行:系统具有在同一时刻处理多个任务的能力。
二. 同步 异步 阻塞 非阻塞
1. 进程间的通信层面
进程间的通信时通过 send() 和 receive() 两种基本操作完成的。具体如何实现这两种基础操作,存在着不同的设计。
消息的传递有可能是阻塞的或非阻塞的 -- 也被称为同步或异步的:
1. 阻塞式发送(blocking send). 发送方进程会被一直阻塞, 直到消息被接受方进程收到。
2. 非阻塞式发送(nonblocking send)。 发送方进程调用 send() 后, 立即就可以其他操作。
3. 阻塞式接收(blocking receive) 接收方调用 receive() 后一直阻塞, 直到消息到达可用。
4. 非阻塞式接受(nonblocking receive) 接收方调用 receive() 函数后, 要么得到一个有效的结果, 要么得到一个空值, 即不会被阻塞。
总结:也就是说, 从进程级通信的维度讨论时, 阻塞和同步(非阻塞和异步)就是一对同义词, 且需要针对发送方和接收方作区分对待。
2. 在 IO 系统调用层面( IO system call )层面
原文链接:https://blog.csdn.net/historyasamirror/article/details/5778378
作者的理论描述和比喻都生动形象。
这里为了方便自己快速回顾,在自认为理解作者原意的基础上,做了所谓的提炼,但是显然是有理解不完全或者错误的地方,希望以后自己看到或者过客看到能指正。
如原文作者所述,这里讨论的背景是Linux环境下的network IO。
总览
在进程通信层面, 阻塞/非阻塞, 同步/异步基本是同义词, 但是需要注意区分讨论的对象是发送方还是接收方。
主要差别在真实IO层面。所谓真实,是因为我们常说的 阻塞/非阻塞/同步/异步IO,其实更具体应该分为两个阶段,如下:
当发起一个IO请求的时候,会经历两个阶段:
1. 等待数据准备
2. 数据准备好后将数据从kernel(内核)拷贝到进程中
所谓的真实的IO层面指的是数据从内核态拷贝到用户态。
阻塞和非阻塞
区别在于**准备数据阶段**(第一阶段)会不会将进程阻塞。
阻塞IO
发起请求后,在这两个阶段进程都会被阻塞
非阻塞IO
发起请求后,在准备数据阶段请求立即返回,因此进程不会被阻塞,但是进程会不断询问数据有没有准备好,准备好之后就发起数据接收的请求,开始第二阶段的数据拷贝,这个阶段中进程仍然会被阻塞
同步和异步
区别在于在进行**IO operation**(第二阶段)的时候会不会将进程阻塞(这里指的是真实的数据IO的时候,比如在将kernel中准备好的数据拷贝到进程中的时候,当在网络IO的时候应该也是类似的意思)。
同步IO:
在做IO operation(指的第二阶段)的时候会阻塞进程
异步IO:
在做IO operation(指的第二阶段)的时候不会阻塞进程,等到整个过程都完成了。
因此,单纯的阻塞IO和非阻塞IO都是同步IO,因为非阻塞IO在第二阶段的时候仍然是被阻塞的,只不过在第一阶段等待数据拷贝的时候是非阻塞的。
三. 协程
https://www.fanhaobai.com/2017/11/synchronised-asynchronized-coroutine.html
1. 对于操作系统来说只有进程和线程,协程的控制由应用程序显式调度,非抢占式的
2. 协程的执行最终靠的还是线程,应用程序来调度协程选择合适的线程来获取执行权
3. 切换非常快,成本低。一般占用栈大小远小于线程(协程 KB 级别,线程 MB 级别),所以可以开更多的协程
4. 协程比线程更轻量级
5. 协程可以理解为用户态的线程
事件循环 event loop
https://rgb-24bit.github.io/blog/2019/python-coroutine-event-loop.html
https://www.bilibili.com/video/av37759434/ [这个是视频,虽然是英文但是有图画还是挺好懂的]
https://zhuanlan.zhihu.com/p/33058983 [这个是一篇知乎上的文章,写得也更详细,也比较易懂]
涉及名称:
+ task queue:任务队列
+ stack:执行栈,负责执行代码,例如渲染,IO等。
+ event loop:事件循环(这其实是一个过程,这个过程被称为事件循环)
event loop 实际上是针对于异步而言的
简述:
当主线程遇到一个异步的事件(协称或者多线程之类的),会将这些任务挂起,并stack中继续执行其他的任务。等到异步事件返回结果后,该事件就会被放到task queue的队尾。当stack中的任务执行完了之后,主线程就把task queue的队首的事件拿出来,并把这个事件的回调放入stack中执行。这整个循环往复的过程就被称为事件循环。
代码举例:
```
console.log('1');
setTimeOut(function cb(){
console.log("2");
}, 0);
console.log('3')
打印结果:
1
3
2
```
在上面这个例子中,明明setTimeOut延时就0,应该打印结果是 1 2 3 才对吧。但是其实,setTimeOut算是个异步,所以虽然延时0,但是它的回调函数cb(也许不能这么叫,但是感觉起来对于像我这样的小白可能容易理解一点)依然被放到了task queue而不是直接进入到stack中,然后event loop等到stack中的所有任务都执行完了,再把task queue中的function cb拿到stack中,然后才执行这个函数。
在py中,我们通过selector注册了事件和回调,但是它并不会自动执行回调, 所以需要我们主动实现一个事件循环,当当前任务结束了,去主动调用它的回调。(至于里面具体的大概可以想象一下,可能是注册完了事件和回调之后,当当前的事件执行之后,回调会被放到task queue,但是并不会被放到stack中,因此这个回调并不会被执行,需要我们实现event loop来把这个回调函数从task queue放到stack中去)
四. GIL锁
http://cenalulu.github.io/python/gil-in-python/
并不是所有的解释器都有GIL锁,例如CPython中有,但是JPython总就没有
全局解释锁 加在解释器上的,为了在解释器层面保证线程安全,所以每个线程想要执行的时候必须获取GIL锁,但是这个锁只有一个,所以导致多线程其实每个cpu只有一个内核可以被使用,所以如果程序是cpu密集型的时候,反而可能不如串行来得效率高,因为多出了许许多多的context switch的时间,如果是io密集型的,那多线程还是很有用的。
线程互斥锁和GIL的区别
1. 线程互斥锁是Python代码层面的锁,解决Python程序中多线程共享资源的问题(线程数据共共享,当各个线程访问数据资源时会出现竞争状态,造成数据混乱);
2. GIL是Python解释层面的锁,解决解释器中多个线程的竞争资源问题(多个子线程在系统资源竞争是,都在等待对象某个部分资源解除占用状态,结果谁也不愿意先解锁,然后互相等着,程序无法执行下去)。
链接:https://juejin.im/post/5b977e5c5188255c996b6fad
解决GIL锁问题
由于python年岁已大,现在很多包都是基于GIL锁的,所以现在去掉可能不太容易。
但是我们可以对不同任务采用不同办法。如下:
1. cpu密集型:可以考虑换一个,别用py了。
2. IO密集型:多线程 或者 多进程(消耗大,通信不方便)+协程