Contextvars and Thread local
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.