Contextvars and Thread local

2019-04-12

Here in the post, I will share some examples about the contextvars (new in Python 3.7) and thread local.

Default Value

In the module level, use ContextVar.set or directly setattr for a thread local variable, won’t successfully set a default value, the value set won’t take effect in another thread.

To ensure a default value, for contextvars

import contextvars
context_var = contextvars.ContextVar("context_var", default=0)

for thread local, a sub class of thread.local need to be declared

class ValueLocal(threading.local):
    def __init__(self, value):
        self.value = value

thread_local = ValueLocal(0)

Behavior with coroutines

If used in the multi threading projects, contextvars and thread local would behave pretty much the same. But with coroutines, using thread local is dangerous and contextvars is the aid.

The example below will show you the different behaviors for contextvars and thread local. First let’s define some base variables and functions for examples for both threads and coroutines.

import time
import contextvars
import threading
import asyncio
import random
import functools

context_var = contextvars.ContextVar("test", default=['root'])

# Set default values for thread.local in new threads
class ValueLocal(threading.local):
    def __init__(self, value):
        self.value = value

thread_local = ValueLocal(['root'])

outputs = {
    "contextvars": "",
    "thread.local": ""
}

def dump_context(x):
    '''
    Dump the contextvars and thread.local to outputs.
    '''
    outputs["contextvars"] += "%s\t%s\n" % (x, '\t'.join(context_var.get()))
    outputs["thread.local"] += "%s\t%s\n" % (x, '\t'.join(thread_local.value))

def async_function_context(f):
    '''
    A wrapper for async/await functions.
    '''
    @functools.wraps(f)
    async def wrapper(*sub, **kwargs):
        context_var.set(context_var.get()[:] + [f.__name__])
        thread_local.value = thread_local.value[:] + [f.__name__]
        r = await f(*sub, **kwargs)
        context_var.set(context_var.get()[:-1])
        thread_local.value = thread_local.value[:-1]
        return r
    return wrapper

def function_context(f):
    '''
    A wrapper of normal functions.
    '''
    @functools.wraps(f)
    def wrapper(*sub, **kwargs):
        context_var.set(context_var.get()[:] + [f.__name__])
        thread_local.value = thread_local.value[:] + [f.__name__]
        r = f(*sub, **kwargs)
        context_var.set(context_var.get()[:-1])
        thread_local.value = thread_local.value[:-1]
        return r
    return wrapper

The first example is for multi threads:

from base import *

@function_context
def foo(x):
    time.sleep(random.random())
    dump_context(x)
    bar(x)
    dump_context(x)

@function_context
def bar(x):
    time.sleep(random.random())
    dump_context(x)
    baz(x)
    dump_context(x)

@function_context
def baz(x):
    time.sleep(random.random())
    dump_context(x)

def main():
    threads = [threading.Thread(target=foo, args=(i,)) for i in [1, 2, 3]]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    for k, v in outputs.items():
        print("***%s***\n%s" % (k, v))

main()

The output results of the above example would be:

***contextvars***
2       root    foo
1       root    foo
3       root    foo
2       root    foo     bar
1       root    foo     bar
3       root    foo     bar
3       root    foo     bar     baz
3       root    foo     bar
3       root    foo
2       root    foo     bar     baz
2       root    foo     bar
2       root    foo
1       root    foo     bar     baz
1       root    foo     bar
1       root    foo

***thread.local***
2       root    foo
1       root    foo
3       root    foo
2       root    foo     bar
1       root    foo     bar
3       root    foo     bar
3       root    foo     bar     baz
3       root    foo     bar
3       root    foo
2       root    foo     bar     baz
2       root    foo     bar
2       root    foo
1       root    foo     bar     baz
1       root    foo     bar
1       root    foo

Results from both contextvars and thread local works are as we expected.

And the second example are for coroutines:

from base import *

@async_function_context
async def foo(x):
    await asyncio.sleep(random.random())
    dump_context(x)
    await bar(x)
    dump_context(x)

@async_function_context
async def bar(x):
    await asyncio.sleep(random.random())
    dump_context(x)
    await baz(x)
    dump_context(x)

@async_function_context
async def baz(x):
    await asyncio.sleep(random.random())
    dump_context(x)

async def main():
    await asyncio.gather(
        foo(1),
        foo(2),
        foo(3)
    )
    for k, v in outputs.items():
        print("***%s***\n%s" % (k, v))

asyncio.run(main())

The results of the above example would be like:

***contextvars***
2       root    foo
1       root    foo
1       root    foo     bar
3       root    foo
1       root    foo     bar     baz
1       root    foo     bar
1       root    foo
2       root    foo     bar
3       root    foo     bar
2       root    foo     bar     baz
2       root    foo     bar
2       root    foo
3       root    foo     bar     baz
3       root    foo     bar
3       root    foo

***thread.local***
2       root    foo     foo     foo
1       root    foo     foo     foo     bar
1       root    foo     foo     foo     bar     bar
3       root    foo     foo     foo     bar     bar     baz
1       root    foo     foo     foo     bar     bar     baz     bar
1       root    foo     foo     foo     bar     bar     baz
1       root    foo     foo     foo     bar     bar
2       root    foo     foo     foo     bar
3       root    foo     foo     foo     bar     baz
2       root    foo     foo     foo     bar     baz     baz
2       root    foo     foo     foo     bar     baz
2       root    foo     foo     foo     bar
3       root    foo     foo     foo
3       root    foo     foo
3       root    foo

This time, the results from contextvars are still as expected. But results from thread local is messed, because different coroutines shares the same thread, breaks the safety of thread local mechanism.