25

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?

8
  • Why create them as tasks, which start running immediately, if you then gather them? Just do the latter Commented Jul 26, 2020 at 21:48
  • 2
    I don't get it. Could you elaborate a little bit please? Commented Jul 26, 2020 at 23:28
  • 4
    contextvars are not a replacement for local variables, those work just fine as they are. They are a replacement for global variables, which are shared among all tasks. If myid were a global variable (say set through a context manager), it would be able to hold only a single value. With threads you can introduce a thread-local variable and have a "global" state that is local to the thread. Contextvars are the equivalent for tasks (and also support callbacks). Commented Jul 27, 2020 at 6:55
  • So basically my tasks are running in the same thread and sharing the same context var. And if I would had more than one thread, then I see how useful is context vars. Am I right? Commented Jul 27, 2020 at 7:29
  • 3
    No. The point of a contextvar is for a single var to have different values in different asyncio tasks and task-like contexts, all running on the same thread. contextvars are not useful for multithreading because they are designed for asyncio which is explicitly single-threaded. Commented Jul 27, 2020 at 9:36

1 Answer 1

52

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

Sign up to request clarification or add additional context in comments.

3 Comments

Hmm, this feels very much like "dynamic scoping" which is a "feature" of emacs-lisp that in practice is pretty handy (though in emacs lisp every global is effectively a context var). It's intersting how this "broken" form of scoping - implemented for simplicity is showing up in elsewhere. This concept also shows up in haskell through the context monad I believe.
So my key takeaway here is: contextvars only "works" with tasks/event loops which explicitly and directly integrate with the machinery of contextvars under the hood, deep inside the internals. Correct? A library which ignores contextvars in its internals would break usercode which relied upon contextvars?
No, you can use contextvars without any frameworks like asyncio. This module is pretty self-sufficient and provides everything you need to manage contexts.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.