/launchpad.py

https://github.com/fluentpython/concurrency · Python · 120 lines · 47 code · 13 blank · 60 comment · 5 complexity · 3945256a5cb1f9c6d2abb8e80172671a MD5 · raw file

  1. #!/usr/bin/env python3
  2. """
  3. Brett Cannon's launchpad: concurrent countdowns driven by a custom event loop,
  4. without `asyncio`. This example shows how `async/await` is independent of
  5. `asyncio` or any specific asynchronous programming library.
  6. Source: "How the heck does async/await work in Python 3.5?"
  7. https://snarky.ca/how-the-heck-does-async-await-work-in-python-3-5/
  8. """
  9. import datetime
  10. import heapq
  11. import types
  12. import time
  13. class Task:
  14. """Represent how long a coroutine should wait before starting again.
  15. Comparison operators are implemented for use by heapq. Two-item
  16. tuples unfortunately don't work because when the datetime.datetime
  17. instances are equal, comparison falls to the coroutine and they don't
  18. implement comparison methods, triggering an exception.
  19. Think of this as being like asyncio.Task/curio.Task.
  20. """
  21. def __init__(self, wait_until, coro):
  22. self.coro = coro
  23. self.waiting_until = wait_until
  24. def __eq__(self, other):
  25. return self.waiting_until == other.waiting_until
  26. def __lt__(self, other):
  27. return self.waiting_until < other.waiting_until
  28. class SleepingLoop:
  29. """An event loop focused on delaying execution of coroutines.
  30. Think of this as being like asyncio.BaseEventLoop/curio.Kernel.
  31. """
  32. def __init__(self, *coros):
  33. self._new = coros
  34. self._waiting = []
  35. def run_until_complete(self):
  36. # Start all the coroutines.
  37. for coro in self._new:
  38. wait_for = coro.send(None)
  39. heapq.heappush(self._waiting, Task(wait_for, coro))
  40. # Keep running until there is no more work to do.
  41. while self._waiting:
  42. now = datetime.datetime.now()
  43. # Get the coroutine with the soonest resumption time.
  44. task = heapq.heappop(self._waiting)
  45. if now < task.waiting_until:
  46. # We're ahead of schedule; wait until it's time to resume.
  47. delta = task.waiting_until - now
  48. time.sleep(delta.total_seconds())
  49. now = datetime.datetime.now()
  50. try:
  51. # It's time to resume the coroutine.
  52. wait_until = task.coro.send(now)
  53. heapq.heappush(self._waiting, Task(wait_until, task.coro))
  54. except StopIteration:
  55. # The coroutine is done.
  56. pass
  57. @types.coroutine
  58. def sleep(seconds):
  59. """Pause a coroutine for the specified number of seconds.
  60. Think of this as being like asyncio.sleep()/curio.sleep().
  61. """
  62. now = datetime.datetime.now()
  63. wait_until = now + datetime.timedelta(seconds=seconds)
  64. # Make all coroutines on the call stack pause; the need to use `yield`
  65. # necessitates this be generator-based and not an async-based coroutine.
  66. actual = yield wait_until
  67. # Resume the execution stack, sending back how long we actually waited.
  68. return actual - now
  69. async def countdown(label, length, *, delay=0):
  70. """Countdown a launch for `length` seconds, waiting `delay` seconds.
  71. This is what a user would typically write.
  72. """
  73. print(label, 'waiting', delay, 'seconds before starting countdown')
  74. delta = await sleep(delay)
  75. print(label, 'starting after waiting', delta)
  76. while length:
  77. print(label, 'T-minus', length)
  78. waited = await sleep(1)
  79. length -= 1
  80. print(label, 'lift-off!')
  81. def main():
  82. """Start the event loop, counting down 3 separate launches.
  83. This is what a user would typically write.
  84. """
  85. loop = SleepingLoop(countdown('A', 5),
  86. countdown('B', 3, delay=2),
  87. countdown('C', 4, delay=1))
  88. start = datetime.datetime.now()
  89. loop.run_until_complete()
  90. print('Total elapsed time is', datetime.datetime.now() - start)
  91. if __name__ == '__main__':
  92. main()