目录

Python性能篇之多进程与多线程的瓶颈,异步IO的到来

一、前言:

  • 我们在之前的章节中学习了多进程与多线程开发的相关知识,如在执行IO操作或者计算量大的任务时我们就可以选择创建进程或者线程去完成相关任务。

补充知识:

  • Python目前主流的解析器是CPython
  • 由于CPython解析器的设计,Python的运行环境中存在有一个全局解析器锁,其限制是在同一时间内,CPython解析器只能运行一个线程的代码。
  • 也就是说,即使你在一个进程中使用了多线程开发,在同一时间内,也只有一个线程的代码会被真正的执行,这大大降低了多线程的性能。
  • 从电脑硬件来说,因为同一时间内只有一个线程被执行,所以,即使你是多核CPU,也只能发挥一个核的性能。
  • 当然,我们可以通过多进程+线程+异步IO来发挥我们CPU的最大性能。

二、同步IO与多进程、线程

  • 我们之所以在执行如文件的读写操作,网络请求,计算密集的任务时选择使用多进程或者多线程,因为如果不使用的话,那么此时的IO操作就属于同步IO了,也就是需要执行完IO操作,后面的代码才会被继续执行。
  • 而CPU的速度远远快于磁盘、网络等,如果遇到同步IO的情况,那么我们的CPU资源就会被极大的浪费,性能就会受到影响。
  • 虽然使用多进程或者多线程解决了同步IO的问题,但是如果需要执行的IO或者网络请求多了,我们也不能无限制的创建大量的进程和线程,首先进程会收到CPU核数的限制,在则系统切换进程线程的开销也不小,一旦数量太大,CPU在切换上消耗的时间就有可能高于运行代码的时间了,此时性能势必会受到影响。这就要求我们适当的创建进程和线程的数量了。

三、异步IO

  • 当然,遇到大量的IO、网络等耗时操作时,我们还有一种解决方案:异步IO
  • 什么是异步IO呢?
  • 异步IO其实就是当我们需要执行一个耗时操作时,如IO操作,它只负责发出IO指令,指令发完了就继续执行后面的代码,不等待IO结果。等IO执行完毕返回结果时,再通知我们进行处理。
  • 我们的异步IO模型需要一个消息循环,这个就很想Andriod系统的运行机制了,一个APP启动后,就会创建一个loop,里面不断的循环,接收消息,然后处理消息,如UI的更新等,其实就是发消息到循环中,然后等待处理。我们的异步IO其实也是一样的原理。
  • 异步IO模式:在消息循环中,主线程不断第重复 读取消息-处理消息 这个操作。

四、协程

  • 协程-Coroutine,也叫微线程,从名字理解,将我们的线程进行了微拆分。
  • 大家都知到,我们的函数调要都是顺序执行的,如 MethodA-》MethodB-》MethodC,然后返回的时时ReturnC-》ReturnB-》ReturnA,其是通过栈结果来实现的,先入栈再出栈
  • 我们的协程也是一个一个方法程序,但是其不是顺序执行的,在一个方法内部可以中断,然后去执行其它程序,在某个点再返回来继续执行,如此变实现了线程的再拆分。

五、Python中实现协程

  • Python其实是通过generator来实现的,也就是生成器。

  • 我们都知道,在generator中,我们可以通过for in 来循环迭代,也可以通过不断调用next()来拿到yield返回的值。其实,yield还有一个特性,它可以接收generator.send()发送出来的数据哦。

  • 试想,我们把要执行IO操作的代码封装成generator,然后放入一个消息循环中执行,当执行完后,我们再执行send操作,是不是我们就可以再yield处接收执行的结果了呢?

  • 我们通过协程来实现一个生产者消费者模型

  • 示例代码如下:

      def consumer(name):
      	r='';
      	while True:
      		n = yield r
      		if not n:
      			return
        print('[Consumer:%s],I am consuming %s...' % (name,n))
      		r = '200 OK'
    
      def produce(name,c):
      	c.send(None)
      	n = 0
        while n < 5:
      		n = n + 1
        print('[Produce:%s], I to producing %s...' % (name,n))
      		r = c.send(n)
      		print('[Produce:%s], Consumer return: %s' % (name,r))
      	c.close()
    
      c = consumer('c1')
      produce('p1',c)
    
  • 结果输入为:

      [Produce:p1], I to producing 1...
      [Consumer:c1],I am consuming 1...
      [Produce:p1], Consumer return: 200 OK
      [Produce:p1], I to producing 2...
      [Consumer:c1],I am consuming 2...
      [Produce:p1], Consumer return: 200 OK
      [Produce:p1], I to producing 3...
      [Consumer:c1],I am consuming 3...
      [Produce:p1], Consumer return: 200 OK
      [Produce:p1], I to producing 4...
      [Consumer:c1],I am consuming 4...
      [Produce:p1], Consumer return: 200 OK
      [Produce:p1], I to producing 5...
      [Consumer:c1],I am consuming 5...
      [Produce:p1], Consumer return: 200 OK
    
  • 可以看到,生产者生产一个,然后通过send通知消费者进行消费。

  • 代码分析:当我们执行c.send(None)的时候,触发生成器,此时r=‘’,当我们下一发送send时,执行yield后面的语句,然后发送下一次的r的值。

  • 如此我们就只在一个线程中通过generator来实现了生产者消费者模型。

  • 实际开发中如此写代码对于开发者来说是比较底层的,所以,python自然会给我们提供了编写协程代码的利器,我们将在后面章节中进行介绍。

六、总结

  • GIL可以说是历史原因遗留下来的问题,并不是python社区不想改变,而是由于太多的库是针对GIL的,因为针对GIL,利用其特性,理想状态下是不会产生线程并发问题的。因为其同一时间只有一个线程在执行嘛。
  • 当然,python现在也是在不断的优化的,相信python的发展会越来越好的。