异步编程

目前为止,我们在做的都是同步编程。同步编程执行过程很简单:一个程序从第一行开始,逐行执行一直到末尾。每次调用一个函数时,程序就会等待这个函数返回然后在执行下一行。

在异步编程中,函数地执行通常是非阻塞的。换句话说,每次你调用一个函数它就会立即返回,但相对得,这就表示函数并不会在其本来的位置被执行。它有了一种机制(名为 调度程序),让处在执行未来任务的函数随时可以由它进行响应。

异步编程的意思就是说在任何异步函数开始之前,程序都有可能结束。通常地解决方法是让异步函数返回“future(未来任务)”或者“promises(预先任务)”。让其标识出这是一个异步函数。最终,有调度程序的异步编程框架阻塞或者说等待这些异步函数完成它们的“future(未来任务)”。

自Python 3.6开始,“asyncio”模块与asyncawait关键字相结合,来让我们写多任务协作程序。在此类编程中,当一个协同函数在顽皮或者等待输入时,都会由yield交出控制权给另一个协同函数。

思考一下下面这个异步函数,它的作用是返回一个数字的平方,并且在返回前会睡眠一秒钟。
异步函数由async def声明。现在先忽略其中的await关键字:

  1. import asyncio
  2. async def square(x):
  3. print('Square', x)
  4. await asyncio.sleep(1)
  5. print('End square', x)
  6. return x * x
  7. # 创建一个事件循环。
  8. loop = asyncio.get_event_loop()
  9. # 执行异步函数并且等待其完成。
  10. results = loop.run_until_complete(square(1))
  11. print(results)
  12. # 将事件循环关闭。
  13. loop.close()

事件循环(https://docs.python.org/3/library/asyncio-eventloop.html )是在有很多事物时,Python可以使用调度机制来执行这些异步函数。我们使用循环来让这些函数运行直到完成。
其中的打印语句是处于同步机制下的,直到我们得到了任何结果(asyncio.sleep(1)结束后)剩下的才会执行。

之前的例子并不能很好地说明异步编程,因为我们没有写得很复杂,而且也只执行了一次函数。不过你可以想一下,如果你要执行square(x)3次呢:

  1. square(1)
  2. square(2)
  3. square(3)

因为square()里面有一个睡眠函数,总执行时间差不多要3秒钟。但每次执行一个函数时计算机就会陷入呆滞,在那一秒钟里什么也不做,我们为什么不能让它在之前的那个函数睡眠时来执行下一个呢?我们稍加改动,把它变成异步:

  1. # 执行异步函数直到其完成。
  2. results = loop.run_until_complete(asyncio.gather(
  3. square(1),
  4. square(2),
  5. square(3)
  6. ))
  7. print(results)

一般来说,我们使用asyncio.gather(*tasks)来让循环等待所有的任务完成。因为协同程序几乎会在同一时间内启动,整个程序只需要1秒钟即可完成。要注意,asyncio.gather()不会按顺序执行协同函数,尽管它会返回一个按顺序排列的列表。

  1. $ python3 python_async.py
  2. Square 2
  3. Square 1
  4. Square 3
  5. End square 2
  6. End square 1
  7. End square 3
  8. [1, 4, 9]

有时我们需要立即处理所返回的结果。对于这种情况,我们可以使用两个协同函数,让它来处理结果,使用asyncio.as_completed()就可以:

  1. (...)
  2. async def when_done(tasks):
  3. for res in asyncio.as_completed(tasks):
  4. print('Result:', await res)
  5. loop = asyncio.get_event_loop()
  6. loop.run_until_complete(when_done([
  7. square(1),
  8. square(2),
  9. square(3)
  10. ]))

打印出的东西差不多是这样的:

  1. Square 2
  2. Square 3
  3. Square 1
  4. End square 3
  5. Result: 9
  6. End square 1
  7. Result: 1
  8. End square 2
  9. Result: 4

最后要说的是,一个异步函数可以使用await关键字来调用另一个异步函数。

  1. async def compute_square(x):
  2. await asyncio.sleep(1)
  3. return x * x
  4. async def square(x):
  5. print('Square', x)
  6. res = await compute_square(x)
  7. print('End square', x)
  8. return res

异步编程部分练习

  1. 写一个异步协同函数,它会将两个变量相加,并且会睡眠这些时间。使用异步循环来调用它。
  2. 修改一下之前的那个程序,让它调度两次。