Upgrade to Pro — share decks privately, control downloads, hide ads and more …

You Might Not Want Async

You Might Not Want Async

First version given at PyCon TW 2016.
Revised for PyCon APAC 2016 (Seoul).
Revised for PyCon JP 2016.

Asynchrony in Python had gathered much momentum recently, with interests from core developers, as evidenced by the introduction of `asyncio` in Python 3.4, and a great boom of related third-party projects following it. By utilising more functionalities from the underlying operating system, it is a great solution to many existing problems in Python applications, gaining practical concurrency without working around the well-known GIL (global interpreter lock) problem.

With all its advantages, asynchrony is, however, still a relatively new concept in Python, and as a result could be somewhat mistaken, even misunderstood by some people. One of these misconceptions, probably the most serious, is to mistake concurrency through asynchrony for parallelism. Although `asyncio` (and other similar solutions) lets multiple parts of your program executes without interfering each other, it does *not* allow them to run together—this is still impossible, at least in CPython, due to the continued existence of the GIL. This makes asynchrony suitable for only a certain, instead of all, kinds of problems. Evaluation is therefore required before a programmer can decide whether the asynchrony model is suitable for a particular application.

Furthermore, partly due to its relatively short existence, paradigms in asynchrony programming do not necessarily fit well with other parts of Python, including libraries, either built-in or third-party ones. Since only blocking libraries were available in most of Python’s history, many assumptions they made may not work well with async programs out-of-the-box. Adopting asynchrony, at least at the present time, will therefore introduce more technical debt to your program. These are all important aspects that require much consideration before you dive head-first into asynchrony.

Tzu-ping Chung

June 04, 2016
Tweet

More Decks by Tzu-ping Chung

Other Decks in Programming

Transcript

  1. You Might Not Want Async

    View Slide

  2. Quick Questions
    • Concurrency with Python
    • Threads
    • Multi-processing
    • Single-thread asynchrony
    • asyncio

    View Slide

  3. ࣗݾ঺հ (PyCon JP Ver.)
    • ৉ ࢠሯ
    • ͫΐ͏ ͪʔͽΜ
    • @uranusjr

    View Slide

  4. Me
    • Call me TP
    • Follow @uranusjr
    • https://uranusjr.com

    View Slide

  5. View Slide

  6. View Slide

  7. http://macdown.uranusjr.com

    View Slide

  8. www. .com

    View Slide

  9. 10–11 June 2017 (Ծ)
    7

    View Slide

  10. View Slide

  11. View Slide

  12. meh

    View Slide

  13. 2014
    Python 3.4 OSDC.tw PyCon APAC
    March April May

    View Slide

  14. 2016

    View Slide

  15. Got me
    thinking

    View Slide

  16. View Slide

  17. Synchronous

    View Slide

  18. (Single-Threaded) Async

    View Slide

  19. Sync vs. Async

    View Slide

  20. Absolute
    magic

    View Slide

  21. Before After

    View Slide

  22. Module 5 - Doctor Faustus by Christopher Marlowe

    View Slide

  23. Not For You
    • Infects the whole program
    • Async is not parallelism
    • Third party support

    View Slide

  24. import sqlite3
    def read_data(dbname):
    con = sqlite3.connect(dbname)
    cur = con.cursor()
    cur.execute('SELECT * FROM data OFFSET 0 LIMIT 1')
    data = cur.fetchone()
    cur.close()
    con.close()
    return data

    View Slide

  25. import aioodbc
    async def read_data(dbname):
    con = await aioodbc.connect(
    dsn='Driver=SQLite;Database={}'.format(dbname),
    )
    cur = await con.cursor()
    await cur.execute('SELECT * FROM data OFFSET 0 LIMIT 1')
    data = await cur.fetchone()
    await cur.close()
    await con.close()
    return data

    View Slide

  26. from .db import read_data
    def main():
    data = read_data('data.sqlite3')
    print(data)
    main()

    View Slide

  27. import asyncio
    from .db import read_data
    async def main():
    data = await read_data('db.sqlite3')
    print(data)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

    View Slide

  28. U+1F937 SHRUG (Unicode 9.0)

    View Slide

  29. RELAX PEOPLE
    I’M JUST GETTING STARTED

    View Slide

  30. import asyncio
    from .db import read_data
    async def main():
    data = read_data('db.sqlite3')
    print(data)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

    View Slide

  31. (demo) $ python demo.py

    View Slide

  32. import asyncio
    from .db import read_data
    async def main():
    data = read_data('db.sqlite3')
    print(data)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

    View Slide

  33. import asyncio
    from .db import read_data
    async def main():
    data = await read_data('db.sqlite3')
    print(data)
    loop = asyncio.get_event_loop()
    loop.run_until_complete(main())
    loop.close()

    View Slide

  34. View Slide

  35. I KNOW
    WRITE UNIT TESTS

    View Slide

  36. GOOD LUCK
    WITH THAT

    View Slide

  37. View Slide

  38. View Slide

  39. import asyncio
    import time
    async def do_something():
    print('Before', time.monotonic())
    await asyncio.sleep(2)
    print('After ', time.monotonic())
     await do_something()
    Before 199208.190632853
    After 199210.192092425

    View Slide

  40. import unittest
    class MyTestCase(unittest.TestCase):
    async def test_do_something(self):
    await do_something()
    if __name__ == '__main__':
    unittest.main()

    View Slide

  41. (demo) $ python tests.py
    .
    ----------------------------------------------------
    Ran 1 test in 0.002s
    OK

    View Slide

  42. (demo) $ python tests.py
    .
    ----------------------------------------------------
    Ran 1 test in 0.002s
    OK

    View Slide

  43. What Happened?
    • unittest does not know about asyncio
    • Coroutine methods are executed “normally”
    • Called, but not executed (awaited)

    View Slide

  44. WHAT IF I TOLD YOU
    THIS IS GONNA BE TOUGH

    View Slide

  45. import asyncio
    import functools
    def asynchronous(func):
    @functools.wraps(func)
    def asynchronous_inner(*args, **kwargs):
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    try:
    loop.run_until_complete(func(*args, **kwargs))
    finally:
    loop.close()
    return asynchronous_inner

    View Slide

  46. import unittest
    class MyTestCase(unittest.TestCase):
    @asynchronous
    async def test_do_something(self):
    await do_something()
    if __name__ == '__main__':
    unittest.main()

    View Slide

  47. (demo) $ python tests.py
    Before 51728.420457105
    After 51730.424515145
    .
    ----------------------------------------------------
    Ran 1 test in 2.006s
    OK

    View Slide

  48. Tips
    • Beware of warning output
    • Especially if you redirect
    • Add coverage report to testing code
    • Consider pip install asynctest

    View Slide

  49. Or use pytest instead

    View Slide

  50. import time
    async def test_do_something():
    before = time.monotonic()
    await do_something()
    delta_t = time.monotonic() - before
    assert -0.01 < delta_t < 0.01

    View Slide

  51. (demo) $ py.test tests.py
    ====================== test session starts ======================
    platform darwin -- Python 3.5.1, pytest-2.9.1, py-1.4.31,
    pluggy-0.3.1
    collected 0 items / 1 errors
    ============================ ERRORS =============================
    ___________________ ERROR collecting tests.py____________________
    > for i, x in enumerate(self.obj()):
    E TypeError: 'coroutine' object is not iterable
    python3.5/site-packages/_pytest/python.py:765: TypeError
    ==================== 1 error in 0.15 seconds ====================

    View Slide

  52. View Slide

  53. import time
    import pytest
    @pytest.mark.asyncio
    async def test_do_something(capsys):
    before = time.monotonic()
    await do_something()
    delta_t = time.monotonic() - before
    assert -0.01 < delta_t < 0.01

    View Slide

  54. View Slide

  55. View Slide

  56. View Slide

  57. asyncio
    • Boilerplate
    • Error-prone
    • Immature API (I think)

    View Slide

  58. import requests
    def collect_contents(urls):
    contents = []
    for url in urls:
    resp = requests.get(url)
    if resp.status_code != 200:
    continue
    content = resp.text
    contents.append(content)
    return contents

    View Slide

  59. import aiohttp
    async def collect_contents(urls):
    contents = []
    with aiohttp.ClientSession() as session:
    for url in urls:
    async with session.get(url) as rest:
    if resp.status != 200:
    continue
    content = await resp.text()
    contents.append(content)
    return contents

    View Slide

  60. 㖶װ׌ַ׵ׁ

    View Slide

  61. import aiohttp
    async def collect_contents(urls):
    contents = []
    with aiohttp.ClientSession() as session:
    for url in urls:
    async with session.get(url) as rest:
    if resp.status != 200:
    continue
    content = await resp.text()
    contents.append(content)
    return contents

    View Slide

  62. View Slide

  63. import asyncio
    import aiohttp
    async def collect_contents(urls):
    coroutines = []
    with aiohttp.ClientSession() as session:
    for url in urls:
    async with session.get(url) as rest:
    if resp.status != 200:
    continue
    coroutines.append(resp.text())
    contents = await asyncio.gather(*coroutines)
    return contents

    View Slide

  64. View Slide

  65. View Slide

  66. View Slide

  67. LET’S TRY SOMETHING
    “DIFFERENT”

    View Slide

  68. Alternatives
    • concurrent & multiprocessing
    • Greenlets
    • Similar idea, but less infectious
    • C extension
    • threading
    • Standard I/O

    View Slide

  69. http://greenlet.readthedocs.io

    View Slide

  70. GEVENT
    MEH

    View Slide

  71. ROUTINES
    ROUTINES EVERYWHERE

    View Slide

  72. package main
    import ("fmt"; "time")
    func doSomething() {
    time.Sleep(2 * time.Second)
    fmt.Println(time.Now(), "Slept")
    }
    func main() {
    doSomething()
    fmt.Println(time.Now(), "OK")
    }
    00:00:00 Slept
    00:00:00 OK

    View Slide

  73. package main
    import ("fmt"; "time")
    func doSomething() {
    time.Sleep(2 * time.Second)
    fmt.Println(time.Now(), "Slept")
    }
    func main() {
    go doSomething()
    fmt.Println(time.Now(), "OK")
    }
    00:00:00 OK

    View Slide

  74. package main
    import ("fmt"; "time")
    var sem = make(chan bool)
    func doSomething() {
    time.Sleep(2 * time.Second)
    fmt.Println(time.Now(), "Slept")
    sem  true
    }
    func main() {
    go doSomething()
    fmt.Println(time.Now(), "OK")
    sem
    }
    00:00:00 OK
    00:00:02 Slept

    View Slide

  75. The Go Model
    • Boilerplate
    • Error-prone
    • Immature API (I think)

    View Slide

  76. """Do something. Synchronous version."""
    import datetime
    import time
    def do_something():
    time.sleep(2)
    print(datetime.datetime.now(), 'Slept')
    do_something()
    print(datetime.datetime.now(), 'Done')

    View Slide

  77. """Do something. Asynchronous version."""
    import asyncio
    import datetime
    async def do_something():
    await asyncio.sleep(2)
    print(datetime.datetime.now(), 'Slept')
    loop = asyncio.get_event_loop()
    task = loop.create_task(do_something())
    print(datetime.datetime.now(), 'Done')
    loop.run_until_complete(asyncio.wait([task]))
    loop.close()

    View Slide

  78. View Slide

  79. """Do something. Synchronous version."""
    import datetime
    import time
    def do_something():
    time.sleep(2)
    print(datetime.datetime.now(), 'Slept')
    do_something()
    print(datetime.datetime.now(), 'Done')

    View Slide

  80. """What if I can just write this?"""
    import asyncio
    import datetime
    import time
    async def do_something():
    await time.sleep(2)
    print(datetime.datetime.now(), 'Slept')
    await do_something()
    print(datetime.datetime.now(), 'Done')
    asyncio.run_event_loop()

    View Slide

  81. I know, it’s not really
    possible.

    View Slide

  82. At least we can dream.
    Or wait until Python 6.0?

    View Slide

  83. Recap
    • Rant
    • Moar rant
    • Susceptible advice
    • Unrealistic dream

    View Slide

  84. But Seriously
    • Asynchrony is not the silver bullet
    • It makes you jump through loops
    • There are alternatives
    • Fingers crossed

    View Slide

  85. View Slide