浅析Python的进程、线程与协程(下)

简介: 进程进程是指在系统中正在运行的一个应用程序,是CPU的最小工作单元。进程有就绪、运行、阻塞、创建和退出五种状态。其中,运行中的三种状态:就绪、运行、阻塞。创建和退出是描述产生和释放的状态。


Python协程

运行效率极高,协程的切换完全由程序控制,不像线程切换需要花费操作系统的开销,线程数量越多,协程的优势就越明显。

同时,在Python中,协程不需要多线程的锁机制,因为只有一个线程,也不存在变量冲突。

协程对于IO密集型任务非常适用,如果是CPU密集型任务,推荐多进程+协程的方式。对于多核CPU,利用多进程+协程的方式,能充分利用CPU,获得极高的性能。

Python协程的发展时间较长:

  • Python2.5 为生成器引用.send()、.throw()、.close()方法
  • Python3.3 为引入yield from,可以接收返回值,可以使用yield from定义协程
  • Python3.4 加入了asyncio模块
  • Python3.5 增加async、await关键字,在语法层面的提供支持
  • Python3.7 使用async def + await的方式定义协程
  • 此后asyncio模块更加完善和稳定,对底层的API进行的封装和扩展
  • Python将于3.10版本中移除以yield from的方式定义协程

示例代码如下:

1. Python2.x 实现协程

Python2.x对协程的支持是通过generator实现的。

在generator中,我们不但可以通过for循环来迭代,还可以不断调用next()函数获取由yield语句返回的下一个值。

但是Python的yield不但可以返回一个值,它还可以接收调用者发出的参数。

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

def consumer():
    print("consumer----------")
    r = ''
    while True:
        n = yield r
        if not n:
            return
        print('[消费者]Consuming %s...' % n)
        r = '200 OK'
def producer(c):
    print("producer----------")
    # 启动生成器
    c.send(None)
    n = 0
    while n < 5:
        n = n + 1
        print('[生产者]Producing %s...' % n)
        r = c.send(n)
        print('[生产者]Consumer return: %s' % r)
    c.close()
if __name__ == '__main__':
    c = consumer()
    producer(c)
复制代码


代码流程说明:

consumer函数是一个generator,把一个consumer传入produce后:

  1. 首先调用c.send(None)启动生成器(在生成带有yield的  generator后 第一个迭代必须是__next__() __next__()  和 send(None) 的效果是相同的);
  2. 然后,一旦生产了东西,通过c.send(n)切换到consumer执行;
  3. consumer通过yield拿到消息,处理,又通过yield把结果传回;
  4. produce拿到consumer处理的结果,继续生产下一条消息;
  5. produce决定不生产了,通过c.close()关闭consumer,整个过程结束。

运行结果:

producer----------
consumer----------
[生产者]Producing 1...
[消费者]Consuming 1...
[生产者]Consumer return: 200 OK
[生产者]Producing 2...
[消费者]Consuming 2...
[生产者]Consumer return: 200 OK
[生产者]Producing 3...
[消费者]Consuming 3...
[生产者]Consumer return: 200 OK
[生产者]Producing 4...
[消费者]Consuming 4...
[生产者]Consumer return: 200 OK
[生产者]Producing 5...
[消费者]Consuming 5...
[生产者]Consumer return: 200 OK
复制代码


send(msg)next()的区别

send可以传递参数给yield表达式,这时传递的参数会作为yield表达式的值,而yield的参数是返回给调用者的值。

换句话说,就是send可以强行修改上一个yield表达式的值。

比如函数中有一个yield赋值a = yield 5,第一次迭代到这里会返回5,a还没有赋值。

第二次迭代时,使用send(10),那么就是强行修改yield 5表达式的值为10,本来是5的,结果a = 10

send(msg)next()都有返回值,它们的返回值是当前迭代遇到yield时,yield后面表达式的值,其实就是当前迭代中yield后面的参数。

第一次调用send时必须是send(None),否则会报错,之所以为None是因为这时候还没有一个yield表达式可以用来赋值。

2. Python3.x 实现协程

要点

  1. 使用async def的形式定义
  2. 在协程中可以使用await关键字,注意,其后跟的是"可等待对象"(协程, 任务 和 Future)
  3. 协程不能直接执行,需要在asyncio.run()中执行,也可以跟在await后面
  4. asyncawait这两个关键字只能在协程中使用
import asyncio
async def foo(name):
   await asyncio.sleep(1)      # 这是一个不会阻塞的sleep,是一个协程
   print(f"name = {name}")
async def main():
   # 协程本身就是一个可等待对象
   await foo("lczmx")  # 执行协程
   print("done")
if __name__ == '__main__':
   # 使用asyncio.run运行
   asyncio.run(main())
复制代码


运行结果:

name = lczmx
done
复制代码


其中,asyncio.run(main, *, debug=False)方法就是对run_until_complete进行了封装:

loop = events.new_event_loop()

return loop.run_until_complete(main)

关于可等待对象说明

可等待对象(awaitable)是能在 await 表达式中使用的对象。可以是协程或是具有__await__() 方法的对象。

那么协程是如何成为可等待对象的呢?

  1. collections.abc.Awaitable类,这是为可等待对象提供的类,可被用于 await 表达式中。
class Awaitable(metaclass=ABCMeta):
   __slots__ = ()
   @abstractmethod
   def __await__(self):   # __await__方法必须返回一个 iterator
      yield
   @classmethod
   def __subclasshook__(cls, C):
      if cls is Awaitable:
         return _check_methods(C, "__await__")
      return NotImplemented
复制代码


  1. async def复合语句创建的函数,它返回的是一个Coroutine对象,而Coroutine继承Awaitable

3. Python3.x 使用协程进行并发操作

使用协程进行并发操作,在Python3.7以上的版本,使用asyncio.create_task(coro)方法,返回一个Task对象,Task类继承Future,在Python3.7以下版本中,使用asyncio.ensure_future(coro_or_future)

import asyncio
async def foo(char:str, count: int):
    for i in range(count):
        print(f"{char}-{i}")
        await asyncio.sleep(1)
async def main():
    task1 = asyncio.create_task(foo("A", 2))
    task2 = asyncio.create_task(foo("B", 3))
    task3 = asyncio.create_task(foo("C", 2))
    await task1
    await task2
    await task3
if __name__ == '__main__':
    asyncio.run(main())
复制代码


执行结果:

A-0
B-0
C-0
A-1
B-1
C-1
B-2
复制代码


总结

  1. 线程和协程推荐在IO密集型的任务(比如网络调用)中使用,而在CPU密集型的任务中,表现较差。
  2. 对于CPU密集型的任务,则需要多个进程,绕开GIL的限制,利用所有可用的CPU核心,提高效率。
  3. 在高并发下的最佳实践就是多进程+协程,既充分利用多核,又充分发挥协程的高效率,可获得极高的性能。
相关文章
|
2天前
|
Java 调度
【Java多线程】对进程与线程的理解
【Java多线程】对进程与线程的理解
10 1
|
2天前
|
调度 Python
探索Python中的异步编程:从回调到协程
本文将介绍Python中的异步编程技术,从最初的回调函数到现代的协程模型。通过对比传统的同步编程方式和异步编程的优劣势,我们深入探讨了Python中异步编程的实现原理,以及如何利用asyncio库和async/await关键字来构建高效的异步应用程序。最后,我们还将讨论一些异步编程的最佳实践和常见问题的解决方法。
|
3天前
|
消息中间件 程序员 调度
Python并发编程:利用多线程提升程序性能
本文探讨了Python中的并发编程技术,重点介绍了如何利用多线程提升程序性能。通过分析多线程的原理和实现方式,以及线程间的通信和同步方法,读者可以了解如何在Python中编写高效的并发程序,提升程序的执行效率和响应速度。
|
5天前
|
监控 Python
python过滤指定进程
python过滤指定进程
13 1
|
5天前
|
运维 监控 Ubuntu
Python实现ubuntu系统进程内存监控
Python实现ubuntu系统进程内存监控
12 1
|
5天前
|
开发者 Python
在Python中查询进程信息的实用指南
在Python中查询进程信息的实用指南
9 2
|
5天前
|
Python
Python中的协程:异步编程的利器
Python中的协程:异步编程的利器
13 1
|
11天前
|
调度
线程和进程的区别?
线程和进程的区别?
|
12天前
|
并行计算 安全 测试技术
Python多线程
【4月更文挑战第13天】对比多线程与多进程:多线程适合I/O密集型任务,轻量级但受GIL限制;多进程适用于CPU密集型任务,能实现真正并行。多线程直接共享内存,多进程独立内存,各有优劣。
9 0
|
12天前
|
缓存 安全 Linux
深入探索Python中的协程
深入探索Python中的协程