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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  5. http://macdown.uranusjr.com

    View full-size slide

  6. 10–11 June 2017 (Ծ)
    7

    View full-size slide

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

    View full-size slide

  8. Got me
    thinking

    View full-size slide

  9. (Single-Threaded) Async

    View full-size slide

  10. Sync vs. Async

    View full-size slide

  11. Absolute
    magic

    View full-size slide

  12. Before After

    View full-size slide

  13. Module 5 - Doctor Faustus by Christopher Marlowe

    View full-size slide

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

    View full-size slide

  15. 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 full-size slide

  16. 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 full-size slide

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

    View full-size slide

  18. 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 full-size slide

  19. U+1F937 SHRUG (Unicode 9.0)

    View full-size slide

  20. RELAX PEOPLE
    I’M JUST GETTING STARTED

    View full-size slide

  21. 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 full-size slide

  22. (demo) $ python demo.py

    View full-size slide

  23. 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 full-size slide

  24. 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 full-size slide

  25. I KNOW
    WRITE UNIT TESTS

    View full-size slide

  26. GOOD LUCK
    WITH THAT

    View full-size slide

  27. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  33. 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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

  37. Or use pytest instead

    View full-size slide

  38. 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 full-size slide

  39. (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 full-size slide

  40. 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 full-size slide

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

    View full-size slide

  42. 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 full-size slide

  43. 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 full-size slide

  44. 㖶װ׌ַ׵ׁ

    View full-size slide

  45. 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 full-size slide

  46. 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 full-size slide

  47. LET’S TRY SOMETHING
    “DIFFERENT”

    View full-size slide

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

    View full-size slide

  49. http://greenlet.readthedocs.io

    View full-size slide

  50. ROUTINES
    ROUTINES EVERYWHERE

    View full-size slide

  51. 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 full-size slide

  52. 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 full-size slide

  53. 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 full-size slide

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

    View full-size slide

  55. """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 full-size slide

  56. """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 full-size slide

  57. """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 full-size slide

  58. """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 full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide

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

    View full-size slide