Search code examples
pythonmultithreadingpython-asynciopython-contextvars

Understanding Python contextvars


Regarding the following SO answer . I've made some changes in order to understand the difference between do use Contextvars and don't.

I expect at some point the variable myid gets corrupted but changing the range to a higher number seems doesn't affect at all.

import asyncio
import contextvars

# declare context var
request_id = contextvars.ContextVar('Id of request.')


async def some_inner_coroutine(myid):
    # get value
    print('Processed inner coroutine of myid   : {}'.format(myid))
    print('Processed inner coroutine of request: {}'.format(request_id.get()))
    if myid != request_id.get():
        print("ERROR")


async def some_outer_coroutine(req_id):
    # set value
    request_id.set(req_id)

    await some_inner_coroutine(req_id)

    # get value
    print('Processed outer coroutine of request: {}'.format(request_id.get()))


async def main():
    tasks = []
    for req_id in range(1, 1250):
        tasks.append(asyncio.create_task(some_outer_coroutine(req_id)))

    await asyncio.gather(*tasks)


if __name__ == '__main__':
    asyncio.run(main())

Results

Processed inner coroutine of myid   : 1
Processed inner coroutine of request: 1
Processed outer coroutine of request: 1
Processed inner coroutine of myid   : 2
Processed inner coroutine of request: 2
Processed outer coroutine of request: 2
Processed inner coroutine of myid   : 3
Processed inner coroutine of request: 3
Processed outer coroutine of request: 3
Processed inner coroutine of myid   : 4
Processed inner coroutine of request: 4
Processed outer coroutine of request: 4
...
...
Processed inner coroutine of myid   : 1244
Processed inner coroutine of request: 1244
Processed outer coroutine of request: 1244
Processed inner coroutine of myid   : 1245
Processed inner coroutine of request: 1245
Processed outer coroutine of request: 1245
Processed inner coroutine of myid   : 1246
Processed inner coroutine of request: 1246
Processed outer coroutine of request: 1246
Processed inner coroutine of myid   : 1247
Processed inner coroutine of request: 1247
Processed outer coroutine of request: 1247
Processed inner coroutine of myid   : 1248
Processed inner coroutine of request: 1248
Processed outer coroutine of request: 1248
Processed inner coroutine of myid   : 1249
Processed inner coroutine of request: 1249
Processed outer coroutine of request: 1249

What should I change to see an unexpected behaviour of the variable myid?


Solution

  • Context variables are convenient when you need to pass a variable along the chain of calls so that they share the same context, in the case when this cannot be done through a global variable in case of concurrency. Context variables can be used as an alternative to global variables both in multi-threaded code and in asynchronous (with coroutines).

    Context variables are natively supported in asyncio and are ready to be used without any extra configuration. Because when a Task is created it copies the current context and later runs its coroutine in the copied context:

    # asyncio/task.py
    class Task:
        def __init__(self, coro):
            ...
            # Get the current context snapshot.
            self._context = contextvars.copy_context()
            self._loop.call_soon(self._step, context=self._context)
    
        def _step(self, exc=None):
            ...
            # Every advance of the wrapped coroutine is done in
            # the task's context.
            self._loop.call_soon(self._step, context=self._context)
            ...
    

    Below is your example showing the corruption of a global variable compared to context vars:

    import asyncio
    import contextvars
    
    # declare context var
    current_request_id_ctx = contextvars.ContextVar('')
    current_request_id_global = ''
    
    
    async def some_inner_coroutine():
        global current_request_id_global
    
        # simulate some async work
        await asyncio.sleep(0.1)
    
        # get value
        print('Processed inner coroutine of request: {}'.format(current_request_id_ctx.get()))
        if current_request_id_global != current_request_id_ctx.get():
            print(f"ERROR! global var={current_request_id_global}")
    
    
    async def some_outer_coroutine(req_id):
        global current_request_id_global
    
        # set value
        current_request_id_ctx.set(req_id)
        current_request_id_global = req_id
    
        await some_inner_coroutine()
    
        # get value
        print('Processed outer coroutine of request: {}\n'.format(current_request_id_ctx.get()))
    
    
    async def main():
        tasks = []
        for req_id in range(1, 10000):
            tasks.append(asyncio.create_task(some_outer_coroutine(req_id)))
    
        await asyncio.gather(*tasks)
    
    
    if __name__ == '__main__':
        asyncio.run(main())
    

    Output:

    ...
    Processed inner coroutine of request: 458
    ERROR! global var=9999
    Processed outer coroutine of request: 458
    
    Processed inner coroutine of request: 459
    ERROR! global var=9999
    Processed outer coroutine of request: 459
    ...
    

    An example of converting code that uses threading.local() can be found in PЕP 567