目录
我竟然妄想在小小的篇幅下把这四件事讲明白,离谱,但又不太离谱~
在多线程、多进程、并行、并发这几个概念中,有很多知识点容易让人第一次看完后还是模棱两可的,这里主要是对概念细节的记录,因为我原来学的时候多少也是似懂非懂的,最近又系统地过了一遍,发现了原来很多认知上的小误解,在这里统一做下梳理与记录。
一、什么是IO操作
IO 即 Input 与 Output 首字母的组合,就是表示对数据的输入输出操作。
第一次接触 IO 是学习单片机的时候,把单片机的引脚叫做 IO 口,单片机的 IO 口操作就是对高低电平的读写操作,但本质上还是对字节流的读写。网络编程中的IO其实也是一样的,都是对字节流的读写操作:读写文件是IO操作、socket/TCP/UDP这些都是IO操作,只要有数据交换的地方都有IO操作。
二、进程、线程、协程
三者间的关系:没有线程的进程是没有灵魂的,协程是线程养的一帮小弟,完全由线程统一调控。
1、进程是资源分配的基本单位
何谓资源分配?资源分配就是系统分配给这个进程的内存空间、CPU、磁盘IO等等。进程本身不能执行任何任务。可以这么理解:进程只是为线程抢了一块肥沃的土地,并配备了一系列齐全的农具,除此之外基本啥也不干了,而对这块地什么时候种什么植物的操作(CPU任务调度)就需要进程通过创建线程来执行了。
2、线程是任务调度的基本单位
何谓任务调度?任务调度就是CPU执行具体的任务(一般是调用函数)。线程是由进程创建的,一个进程至少有一个线程,没有线程的进程就是 si 的,啥也干不了!所以说线程是进程的灵魂,同时,只要有线程存在,其就必须依附于一个进程上,也就是说没有肉体依附的灵魂卵用没有~
对于打开一个APP来说,这个APP就是一个进程,而在这个APP上播放音乐、即时聊天等就是由这个进程创建的一个个线程实现的。
3、协程—由线程创建并全权负责的小弟
协程是由线程创建的执行体,可以理解为线程创建的“小线程”。协程的切换同样需要现场保护、恢复现场等操作,但协程比线程更轻量,由于不需要操作系统交互,其切换开销要比线程切换开销小;而线程的切换除了寄存器上下文的切换,还需要进行优先级、内核态进出等等更多与操作系统间的交互。
用户程序不能操作内核空间,只能给协程分配用户栈,而操作系统对协程一无所知,所以协程又被称为用户态线程。
协程多用于IO多路复用的场景。
三、并行、并发及其与CPU内核数的关系
多线程、高并发,对于初学者来说确实是云里雾里的。多线程就一定是并行执行的吗?并行和并发有什么区别?为什么是高并发,咋不叫高并行呢?并行执行的条件是啥呢?这些都是需要搞明白的。
1、理解并行与并发的区别
并发使用了CPU时间片轮转机制,并不是真正的同时执行,只是轮转很快让人感觉是在“同时执行”所以才叫“并发”。并行才是真正的同时执行。
2、进程、线程与CPU核数之间的关系
拥有2个运算设备的CPU称作双核CPU,拥有4个运算器的CPU称作4核CPU,以此类推。也就是说,一个CPU中可能包含多个运算设备(核)。核的个数与可同时运行的线程数(并行)相同。相反,若线程数超过核数,线程将分时使用CPU资源(并发)。但因为CPU运转速度极快,我们会感到所有进程同时运行。当然,核数越多这种感觉越明显。
3、同一个进程里的线程可以并行执行吗(满足条件时可以)
当系统有一个以上核心时,则同一个进程里的两个线程的操作有可能并行执行。当一个核心执行一个线程时,另一个核心可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行。
这里得再明确一个问题:进程只管资源分配,线程才是任务调度的基本单位,因此最终程序的执行,无论是并行还是并发都是体现在线程上的!CPU看到的也只是线程!
但是说进程可以并行执行也没问题,因为有进程存在就一定会有线程存在,如果两个进程里面的线程是并行执行的话就越等于这两个进程在并行执行了。但是你得清楚,实际在运行的是线程。
总结就是:
单核CPU,线程只能并发执行;多核CPU,线程和进程以及同一个进程中的线程都可以并行执行,当线程或进程数量多余核心数时,一般是并行并发同时存在的
但是在 Python 中,无论是单核还是多核,一个进程同时只能由一个线程在执行。其根源是 GIL 的存在。GIL 的全称是 Global Interpreter Lock(全局解释器锁),来源是 Python 设计之初的考虑,为了数据安全所做的决定。某个线程想要执行,必须先拿到 GIL,并且在一个 Python 进程中,GIL 只有一个。拿不到GIL的线程,就不允许进入 CPU 执行。因此python中想要实现并行执行,只能用多进程实现。
对于计算复杂型的运算(如矩阵运算),我们要尽可能多地让代码并行执行.对于IO密集型的操作,尽可能多地使用并发,提高CPU的使用率,从而提高CPU的运行效率
- 创建和销毁较频繁使用线程,因为创建进程花销大。
- 需要大量数据传送使用线程,因为多线程切换速度快,不需要跨越进程边界。
- 安全稳定选进程;快速频繁选线程。
四、IO多路复用
(建议搭配结尾的视频食用,以下内容多少借鉴了这些链接中的视频)
socket 接受客户端连接,并返回给应用程序一个文件描述符fd,每个文件描述符就是一个数,代表文件描述符的编号,用于识别不用的socket(返回给应用程序的只有一个socket描述符)。每个TCP在操作时操作系统都会为其在内核空间分配一个读缓冲区和一个写缓冲区,要获得相应数据就要从都缓冲区拷贝到用户空间中,同样的要通过socket发送数据,也要先把数据拷贝到写缓冲区才行。
(图片来源: 【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili)
对于大量IO请求有以下几种处理方式:
- 阻塞等待+多线程/多进程:需要开辟线程浪费资源
阻塞式IO(没有数据来时就让出CPU)在高并发情况下会增加调度开销。
- 非阻塞+忙轮询:占用CPU(while和for),CPU利用率不高
非阻塞式IO(不让出CPU)用忙轮询方式,挨个尝试访问,但是需要频繁检查SOCKET是否就绪,很难把握轮询的间隔时间,容易造成空耗CPU,加剧响应延迟。
- 多路IO复用
IO多路复用,由操作系统提供支持,把需要等待的socket加入到监听集合,这样就可以通过一次系统调用同时监听多个socket,有socket就绪了就可以逐个处理了,既不用为等待某个socket而阻塞,也不会陷入忙等待之中。
Linux实现了三种IO复用实现方式:select / poll / epoll,这属于拓展内容了,有兴趣可以查看后面的链接。
五、参考文(zi)献(liao)
进程、线程、并行、并发参考资料:
一个视频告诉你“并发、并行、异步、同步”的区别_哔哩哔哩_bilibili
cpu的核数和进程_多线程,多进程,多核总结_玉漏迟的博客-CSDN博客
多CPU,多核,多进程,多线程 - 苏铭枫 - 博客园 (cnblogs.com)
进程、线程和CPU 之间的关系(一)_nandao158的博客-CSDN博客_线程和cpu
协程:
【协程第一话】协程到底是怎样的存在?_哔哩哔哩_bilibili
IO多路复用参考资料:
深入理解Linux中网络I/O复用并发模型_哔哩哔哩_bilibili
【协程第二话】协程和IO多路复用更配哦~_哔哩哔哩_bilibili
【并发】IO多路复用select/poll/epoll介绍_哔哩哔哩_bilibili
协程与IO多路复用