Skip to content

Instantly share code, notes, and snippets.

@allemangD
Last active November 1, 2018 15:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save allemangD/bba8dc2d059310623f752ebf65bb6cdc to your computer and use it in GitHub Desktop.
Save allemangD/bba8dc2d059310623f752ebf65bb6cdc to your computer and use it in GitHub Desktop.
Show how Context Managers do not correctly support suspended execution
import asyncio
class CM:
mode = 0
def __init__(self, m):
self.m = m
def __enter__(self):
self.old = CM.mode
CM.mode = self.m
def __exit__(self, *_):
CM.mode = self.old
async def long_op():
with CM('long'):
print(f'- - mode {CM.mode}')
await asyncio.sleep(3)
print(f'- - mode {CM.mode} done')
async def short_op():
with CM('short'):
for i in range(5):
print(f'{i} - mode {CM.mode}')
await asyncio.sleep(1)
print(f'{i} - mode {CM.mode} done')
async def main():
await asyncio.gather(short_op(),
long_op())
asyncio.run(main())
@allemangD
Copy link
Author

allemangD commented Nov 1, 2018

Note that the short_op iterations have the wrong mode while long_op is waiting.

with CM('short'):
    for i in range(5):
        print(f'{i} - mode {CM.mode}')
        await asyncio.sleep(1)
        print(f'{i} - mode {CM.mode} done')

You would expect this to have mode be 'short' for all iterations, but this is not the case - the value of mode depends on the context manager in long(). Note how the short() iterations have the wrong mode while long() is still waiting:

0 - mode short
- - mode long
0 - mode long done
1 - mode long
1 - mode long done
2 - mode long
- - mode long done
2 - mode short done
3 - mode short
3 - mode short done
4 - mode short
4 - mode short done

The expected output is:

0 - mode short
- - mode long
0 - mode short done
1 - mode short
1 - mode short done
2 - mode short
- - mode long done
2 - mode short done
3 - mode short
3 - mode short done
4 - mode short
4 - mode short done

Where each iteration of short_op occurs in the correct context, regardless of the state of long_op. This would be accomplished by extending the definition of CM with __suspend__ and __resume__ methods:

class CM:
    mode = 0
    
    def __init__(self, m):
        self.m = m

    def __enter__(self):
        self.old = CM.mode
        CM.mode = self.m

    def __resume__(self):
        self.old = CM.mode
        CM.mode = self.m

    def __exit__(self, *_):
        CM.mode = self.old

    def __suspend__(self):
        CM.mode = self.old

Or by using a suspendable decorator:

@suspendable
class CM:
    mode = 0
    
    def __init__(self, m):
        self.m = m

    def __enter__(self):
        self.old = CM.mode
        CM.mode = self.m

    def __exit__(self, *_):
        CM.mode = self.old

@1st1
Copy link

1st1 commented Nov 1, 2018

This is how your example can be fixed with the contextvars module:

import asyncio
import contextvars

cm_mode = contextvars.ContextVar('cm_mode', default=None)

class CM:

    def __init__(self, m):
        self.m = m

    @property
    def mode(self):
        return cm_mode.get()

    def __enter__(self):
        self.old = cm_mode.get()
        cm_mode.set(self.m)
        return self

    def __exit__(self, *_):
        cm_mode.set(self.old)


async def long_op():
    with CM('long') as cm:
        print(f'- - mode {cm.mode}')
        await asyncio.sleep(3)
        print(f'- - mode {cm.mode} done')

async def short_op():
    with CM('short') as cm:
        for i in range(5):
            print(f'{i} - mode {cm.mode}')
            await asyncio.sleep(1)
            print(f'{i} - mode {cm.mode} done')


async def main():
    await asyncio.gather(short_op(),
                         long_op())

asyncio.run(main())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment