进程、线程与协程

思考:

​ 我最近一直在做优化的问题,数据库操作优化还有加速爬取数据。 之前用过多进程、多线程、和协程池,因此考虑再次 用出来。 因为考虑过少,对于这两个问题我都使用了多进程+协程池,但是两个问题的效果差别很大, 爬取数据问题得到了解决。但是数据库优化还是没有很大进展。 归根结底还是底层的基础知识打的不牢,对多进程、多线程、协程池的原理和适用范围不清楚, 因此,在此对这些基础进行学习。

学习

1.参考链接

https://www.cnblogs.com/guolei2570/p/8810536.html

https://blog.csdn.net/daaikuaichuan/article/details/82951084

https://www.liaoxuefeng.com/wiki/897692888725344/923057403198272

https://zhuanlan.zhihu.com/p/70256971

https://mp.weixin.qq.com/s?__biz=MzIxMjY5NTE0MA==&mid=2247483720&idx=1&sn=f016c06ddd17765fd50b705fed64429c&scene=21#wechat_redirect

2.操作系统的设计

多进程形式,允许多个任务同时运行

多线程形式,允许单个任务分成不同的部分运行

提供协调机制,一方面防止进程之间和线程之间产生冲突,另一方面允许进程之间和线程之间共享资源

3.进程

是资源分配的基本单位;

进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动

一个进程最少包含一个线程,也可以包含多个线程

通信方式:
管道(Pipe)、命名管道(FIFO)、消息队列(Message Queue) 、信号量(Semaphore) 、共享内存(Shared Memory);套接字(Socket)。

4.线程

是操作系统调度(CPU调度)执行的最小单位

​ 线程有自己的栈,这个栈仍然是使用进程的地址空间,只是这块空间被线程标记为了栈。每个线程都会有自己私有的栈,这个栈是不可以被其他线程所访问的

5.进程与线程之间的联系

调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位;

并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行;

拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。进程所维护的是程序所包含的资源(静态资源), 如:地址空间,打开的文件句柄集,文件系统状态,信号处理handler等;线程所维护的运行相关的资源(动态资源),如:运行栈,调度相关的控制信息,待处理的信号集等;

系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。但是进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个进程死掉就等于所有的线程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。

一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程;

资源分配给进程,同一进程的所有线程共享该进程的所有资源;

处理机分给线程,即真正在处理机上运行的是线程;

线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  • 进程与线程的亲缘性

    亲缘性的意思是进程/线程只在某个cpu上运行(多核系统)

    BOOL WINAPI SetProcessAffinityMask(
      _In_ HANDLE    hProcess,
      _In_ DWORD_PTR dwProcessAffinityMask
    );
    /*
    dwProcessAffinityMask 如果是 0 , 代表当前进程只在cpu0 上工作;
    如果是 0x03 , 转为2进制是 00000011 . 代表只在 cpu0 或 cpu1上工作;
    */
    
    

    使用CPU亲缘性的好处:设置CPU亲缘性是为了防止进程/线程在CPU的核上频繁切换,从而避免因切换带来的CPU的L1/L2 cache失效,cache失效会降低程序的性能。

这副图是一个双向多车道的道路图,假如我们把整条道路看成是一个“进程”的话,那么图中由白色虚线分隔开来的各个车道就是进程中的各个“线程”了。
①这些线程(车道)共享了进程(道路)的公共资源(土地资源)。
②这些线程(车道)必须依赖于进程(道路),也就是说,线程不能脱离于进程而存在(就像离开了道路,车道也就没有意义了)。
③这些线程(车道)之间可以并发执行(各个车道你走你的,我走我的),也可以互相同步(某些车道在交通灯亮时禁止继续前行或转弯,必须等待其它车道的车辆通行完毕)。
④这些线程(车道)之间依靠代码逻辑(交通灯)来控制运行,一旦代码逻辑控制有误(死锁,多个线程同时竞争唯一资源),那么线程将陷入混乱,无序之中。
⑤这些线程(车道)之间谁先运行是未知的,只有在线程刚好被分配到CPU时间片(交通灯变化)的那一刻才能知道

这个例子让我有了新的idea, 我现在的需求就是有8个子文件,一个总文件, 需要判断总文件中的每一个IP是否再8个子文件中出现。那我就先试试一个进程,8个线程?

用shell?? grep?

5.协程

协程,是一种比线程更加轻量级的存在,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。

子程序,或者称为函数,在所有语言中都是层级调用,比如A调用B,B在执行过程中又调用了C,C执行完毕返回,B执行完毕返回,最后是A执行完毕。所以子程序调用是通过栈实现的,一个线程就是执行一个子程序。子程序调用总是一个入口,一次返回,调用顺序是明确的。而协程的调用和子程序不同。

协程在子程序内部是可中断的,然后转而执行别的子程序,在适当的时候再返回来接着执行

  • 相比多线程的优势:

    极高的执行效率:因为子程序切换不是线程切换,而是由程序自身控制,因此,没有线程切换的开销,和多线程比,线程数量越多,协程的性能优势就越明显;
    
  • 不需要多线程的锁机制:因为只有一个线程,也不存在同时写变量冲突,在协程中控制共享资源不加锁,只需要判断状态就好了,所以执行效率比多线程高很多

  • 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈。因此:协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态,换种说法:进入上一次离开时所处逻辑流的位置。

  • 协程的好处:

    1. 无需线程上下文切换的开销
    2. 无需原子操作锁定及同步的开销
    3. 方便切换控制流,简化编程模型

    高并发+高扩展性+低成本:一个CPU支持上万的协程都不是问题。所以很适合用于高并发处理。

    协程的缺点

    1. 无法利用多核资源:协程的本质是个单线程,它不能同时将 单个CPU 的多个核用上,协程需要和进程配合才能运行在多CPU上.当然我们日常所编写的绝大部分应用都没有这个必要,除非是cpu密集型应用。
    2. 进行阻塞(Blocking)操作(如IO时)会阻塞掉整个程序

6.协程如何利用多核CPU

最简单的方法是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

python对协程的支持还非常有限,用在generator中的yield可以一定程度上实现协程。虽然支持不完全,但已经可以发挥相当大的威力了。

来看例子:

传统的生产者-消费者模型是一个线程写消息,一个线程取消息,通过锁机制控制队列和等待,但一不小心就可能死锁。

如果改用协程,生产者生产消息后,直接通过yield跳转到消费者开始执行,待消费者执行完毕后,切换回生产者继续生产,效率极高:

import time

def consumer():
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[CONSUMER] Consuming %s...' % n)
        time.sleep(1)
        r = '200 OK'

def produce(c):
    c.next()
    n = 0
    while n < 5:
        n = n + 1
        print('[PRODUCER] Producing %s...' % n)
        r = c.send(n)
        print('[PRODUCER] Consumer return: %s' % r)
    c.close()

if __name__=='__main__':
    c = consumer()
    produce(c)

执行结果:

[PRODUCER] Producing 1...
[CONSUMER] Consuming 1...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 2...
[CONSUMER] Consuming 2...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 3...
[CONSUMER] Consuming 3...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 4...
[CONSUMER] Consuming 4...
[PRODUCER] Consumer return: 200 OK
[PRODUCER] Producing 5...
[CONSUMER] Consuming 5...
[PRODUCER] Consumer return: 200 OK

注意到consumer函数是一个generator(生成器),把一个consumer传入produce后:

  1. 首先调用c.next()启动生成器;
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

整个流程无锁,由一个线程执行,produce和consumer协作完成任务,所以称为“协程”,而非线程的抢占式多任务。

最后套用Donald Knuth的一句话总结协程的特点:

“子程序就是协程的一种特例。”

7.并发

并发:在操作系统中,某一时间段,几个程序在同一个CPU上运行,但在任意一个时间点上,只有一个程序在CPU上运行。

8.并行

当操作系统有多个CPU时,一个CPU处理A线程,另一个CPU处理B线程,两个线程互相不抢占CPU资源,可以同时进行,这种方式成为并行。

  • 并发描述的是程序的组织结构。指程序要被设计成多个可独立执行的子任务。
  • 以利用有限的计算机资源使多个任务可以被实时或近实时执行为目的。
你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。
你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。
你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。
并发的关键是你有处理多个任务的能力,不一定要同时。并行的关键是你有同时处理多个任务的能力。所以我认为它们最关键的点就是:是否是『同时』
  • 并行描述的是程序的执行状态。指多个任务同时被执行。
  • 以利用富余计算资源(多核CPU)加速完成多个任务为目的。

并发提供了一种程序组织结构方式,让问题的解决方案可以并行执行,但并行执行不是必须的

并行是为了利用多核加速多任务完成的进度

并发是为了让独立的子任务都有机会被尽快执行,但不一定能加速整体进度

非阻塞是为了提高程序整体执行效率

异步是高效地组织非阻塞任务的方式

9.单核CPU是否能运行多进程?

单核CPU也可以运行多进程,只不过不是同时的,而是极快地在进程间来回切换实现的多进程

10.多进程与多线程的使用

CPU密集型代码(各种循环处理、计算等等):使用多进程。IO密集型代码(文件处理、网络爬虫等):使用多线程

  1. 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
  2. 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
  3. 所以大并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。

11.阻塞与非阻塞

阻塞是指调用线程或者进程被操作系统挂起。

  • 程序未得到所需计算资源时被挂起的状态。
  • 程序在等待某个操作完成期间,自身无法继续干别的事情,则称该程序在该操作上是阻塞的。
  • 常见的阻塞形式有:网络I/O阻塞、磁盘I/O阻塞、用户输入阻塞等。

非阻塞是指调用线程或者进程不会被操作系统挂起。

  • 程序在等待某操作过程中,自身不被阻塞,可以继续运行干别的事情,则称该程序在该操作上是非阻塞的。
  • 非阻塞并不是在任何程序级别、任何情况下都可以存在的。
  • 仅当程序封装的级别可以囊括独立的子程序单元时,它才可能存在非阻塞状态。

非阻塞的存在是因为阻塞存在,正因为某个操作阻塞导致的耗时与效率低下,我们才要把它变成非阻塞的。

12.同步与异步

同步是阻塞模式,异步是非阻塞模式。

  • 同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,知道收到返回信息才继续执行下去;
  • 同程序单元为了完成某个任务,在执行过程中需靠某种通信方式以协调一致同步意味着有序
  • 异步是指进程不需要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回式系统会通知进程进行处理,这样可以提高执行的效率。

由调用方盲目主动问询的方式是同步调用,由被调用方主动通知调用方任务已完成的方式是异步调用。

  • 为完成某个任务,不同程序单元之间过程中无需通信协调,也能完成任务的方式。
  • 不相关的程序单元之间可以是异步的。
  • 例如,爬虫下载网页。调度程序调用下载程序后,即可调度其他任务,而无需与该下载任务保持通信以协调行为。不同网页的下载、保存等操作都是无关的,也无需相互通知协调。这些异步操作的完成时刻并不确定。
  • 简言之,异步意味着无序

13.异步编程

以进程、线程、协程、函数/方法作为执行任务程序的基本单位,结合回调、事件循环、信号量等机制,以提高程序整体执行效率和并发能力的编程方式。
  • 几乎所有的异步框架都将异步编程模型简化一次只允许处理一个事件。故而有关异步的讨论几乎都集中在了单线程内
  • 一旦采取异步编程,每个异步调用必须“足够小”,不能耗时太久。如何拆分异步任务成了难题。

14.CPU的时间观

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PqVen2pZ-1618403888170)(640)]

哈哈哈哈哈 浪费“CPU”的时间等于谋财害命。而凶手就是程序猿。

提高效率,I/O是最大的瓶颈

网络I/O是最大的I/O瓶颈

15.异步IO

进化历程

以python网络爬虫为例

同步阻塞方式

最容易想到的解决方案就是依次下载,从建立socket连接到发送网络请求再到读取响应数据,顺序进行。

建网络连接需要1秒钟,那么sock.connect()就得阻塞1秒钟,等待网络连接成功。这1秒钟对一颗2.6GHz的CPU来讲,仿佛过去了83年,然而它不能干任何事情。
多进程

​ 会有提升,比如开十个进程,但是总体耗时并没有缩减到原来的十分之一,因为存在进程切换开销。

当进程数量大于CPU核心数量时,进程切换是必然需要的。

​ 缺点:

切换开销,多进程还有另外的缺点。一般的服务器在能够稳定运行的前提下,可以同时处理的进程数在数十个到数百个规模。如果进程数量规模更大,系统运行将不稳定,而且可用内存资源往往也会不足。
多线程

由于线程的数据结构比进程更轻量级,同一个进程可以容纳多个线程,从进程到线程的优化由此展开。后来的OS也把调度单位由进程转为线程,进程只作为线程的容器,用于管理进程所需的资源。而且OS级别的线程是可以被分配到不同的CPU核心同时运行的。

在做阻塞的系统调用时,例如sock.connect(),sock.recv()时,当前线程会释放GIL,让别的线程有执行机会。但是单个线程内,在阻塞调用上还是阻塞的。

除了GIL之外,所有的多线程还有通病。它们是被OS调度,调度策略是抢占式的,以保证同等优先级的线程都有均等的执行机会,那带来的问题是:并不知道下一时刻是哪个线程被运行,也不知道它正要执行的代码是什么。所以就可能存在竞态条件。这是多线程的主要问题。

非阻塞方式

非阻塞就是在做一件事的时候,不阻碍调用它的程序做别的事情。

epoll

OS将I/O状态的变化都封装成了事件,如可读事件、可写事件。并且提供了专门的系统模块让应用程序可以接收事件通知。这个模块就是select。让应用程序可以通过select注册文件描述符和回调函数。当文件描述符的状态发生变化时,select 就调用事先注册的回调函数。select因其算法效率比较低,后来改进成了poll,再后来又有进一步改进,BSD内核改进成了kqueue模块,而Linux内核改进成了epoll`模块

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值