Python协程的用法(附带实例)
众所周知,CPU 的运行速度远远快于硬盘、网络等 IO 操作的速度。在单进程、单线程程序中,一旦遇到文件读写、网络通信等 IO 操作,程序的其他代码就需要等待 IO 操作完成才能继续执行,这种情况被称为同步 IO。可见,纵使 CPU 执行代码的速度很快,在同步 IO 程序中,CPU 资源也是白白浪费的。
由于 IO 操作阻塞了当前线程,导致其他代码无法执行,因此采用多进程或者多线程来并发执行代码,当一个线程遇到 IO 操作被挂起时,其他线程将获得 CPU 资源而继续执行,通过多进程或多线程实现了多个任务,高效地利用了 CPU 的资源。
多进程和多线程的方式虽然解决了并发问题,但是随着多进程和多线程数量的不断增多,它们之间切换的资源消耗巨大。CPU 的运行时间大量消耗在切换上,用于执行代码的时间就减少了,同样浪费了计算机的资源。
为了实现并发和多个任务,多进程和多线程只是解决该问题的一种办法。Python 还提供了更优的解决方案,那就是协程和异步IO。
这样解释起来可能不容易理解,可以使用实际代码来讲解,通过协程改写之前生产者和消费者的程序 queue_thread.py,编写程序代码,保存在文件 yield.py 中,即:
通过协程的使用同样实现了生产者和消费者的模型,程序在一个线程中执行。
修改后的 yield.py 代码为:
通过 Linux 自带的 time 命令测试以上两个程序的运行耗时,结果为:
可以看出,多线程代码是协程代码运行时间的两倍,将循环次数扩大到 100000 继续测试,结果为:
与多线程相比,协程有如下优势:
由于协程在一个线程内执行,因此对于多核 CPU 平台,当并发量很大时,可以用多进程+协程的编程方式,既可充分利用多核 CPU,又可充分发挥协程的高效率,程序可以获得极高的性能。
由于 IO 操作阻塞了当前线程,导致其他代码无法执行,因此采用多进程或者多线程来并发执行代码,当一个线程遇到 IO 操作被挂起时,其他线程将获得 CPU 资源而继续执行,通过多进程或多线程实现了多个任务,高效地利用了 CPU 的资源。
多进程和多线程的方式虽然解决了并发问题,但是随着多进程和多线程数量的不断增多,它们之间切换的资源消耗巨大。CPU 的运行时间大量消耗在切换上,用于执行代码的时间就减少了,同样浪费了计算机的资源。
为了实现并发和多个任务,多进程和多线程只是解决该问题的一种办法。Python 还提供了更优的解决方案,那就是协程和异步IO。
Python协程
协程(Coroutine)是可以协同运行的例程,类似于 CPU 的中断,可由用户调度,可在一个子程序内中断去执行其他的子程序。这样解释起来可能不容易理解,可以使用实际代码来讲解,通过协程改写之前生产者和消费者的程序 queue_thread.py,编写程序代码,保存在文件 yield.py 中,即:
def Producer(c): c.send(None) n = 0 while n < 5: n += 1 print('Producer has created %s' % n) data = c.send(n) print('Consumer return; %s' % data) c.close() def Consumer(): data = '' count = 0 while count < 6: count += 1 content = yield data if not content: return print('Consumer has used %s' % content) data = 'done' c = Consumer() Producer(c)运行结果为:
Producer has created 1
Consumer has used 1
Consumer return; done
Producer has created 2
Consumer has used 2
Consumer return; done
Producer has created 3
Consumer has used 3
Consumer return; done
Producer has created 4
Consumer has used 4
Consumer return; done
Producer has created 5
Consumer has used 5
Consumer return; done
- 首先定义 Producer() 和 Consumer() 两个函数,Consumer() 是一个生成器,将 Consumer 传入 Producer;
- 在 Producer() 函数中通过 c.send(None) 启动生成器;
- 在 Producer() 函数中生成一个数,通过 c.send(n) 切换到 Consumer 去执行;
- Consumer 通过 yield 接收来自 Producer 的数据,之后通过 yield 返回 'done';
- Producer 继续从 c.send(n) 后的代码开始执行,重复执行第 3 步和第 4 步,直到 Producer 退出循环;
- Producer 通过 c.close() 关闭 Consumer,程序结束。
通过协程的使用同样实现了生产者和消费者的模型,程序在一个线程中执行。
Python协程与多线程对比
上一节通过协程 yield.py 改写了生产者和消费者的程序,修改后的 queue_thread.py 代码为:import threading, time import queue q = queue.Queue() def Producer(): n = 0 while n < 1000: n += 1 q.put(n) # print('Producer has created %s' % n) # time.sleep(0.1) def Consumer(): count = 0 while count < 1000: count += 1 data = q.get() # print('Consumer has used %s' % data) # time.sleep(0.2) p = threading.Thread(target = Producer, name="") c = threading.Thread(target = Consumer, name="")
修改后的 yield.py 代码为:
def Producer(c): c.send(None) n = 0 while n < 1000: n += 1 # print('Producer has created %s' % n) data = c.send(n) # print('Consumer return; %s' % data) c.close() def Consumer(): data = '' count = 0 while count < 1000: count += 1 content = yield data if not content: return # print('Consumer has used %s' % content) data = 'done' c = Consumer() Producer(c)
通过 Linux 自带的 time 命令测试以上两个程序的运行耗时,结果为:
# time -p python ./queue_thread.py real 0.04 user 0.03 sys 0.00 # time -p python ./yield.py real 0.02 user 0.02 sys 0.00测试结果的含义如下:
- real 表示的是执行脚本的总时间;
- user 表示的是执行脚本消耗CPU的时间;
- sys 表示的是执行内核函数消耗的时间。
可以看出,多线程代码是协程代码运行时间的两倍,将循环次数扩大到 100000 继续测试,结果为:
# time -p python ./queue_thread.py real 0.72 user 0.68 sys 0.01 # time -p python ./yield.py real 0.05 user 0.04 sys 0.00从测试结果可以看出,随着循环次数的扩大,多线程效率与协程差距更加巨大。
与多线程相比,协程有如下优势:
- 协程执行效率更高。因为子程序切换不是线程切换,是由程序自身控制的,所以没有线程切换的开销,线程数量越多,协程的性能优势越明显。
- 协程运行更加稳定。因为协程在同一个线程内执行,不存在同时写变量的冲突,在协程中控制共享资源不需要加锁,只需要判断状态,可避免产生死锁的风险,且执行效率比多线程高得多。
由于协程在一个线程内执行,因此对于多核 CPU 平台,当并发量很大时,可以用多进程+协程的编程方式,既可充分利用多核 CPU,又可充分发挥协程的高效率,程序可以获得极高的性能。