Skip to content

Commit f13f12d

Browse files
author
Yury Selivanov
authored
bpo-32630: Use contextvars in decimal (GH-5278)
1 parent bc4123b commit f13f12d

4 files changed

Lines changed: 70 additions & 100 deletions

File tree

‎Lib/_pydecimal.py‎

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -433,34 +433,32 @@ class FloatOperation(DecimalException, TypeError):
433433
# The getcontext() and setcontext() function manage access to a thread-local
434434
# current context.
435435

436-
import threading
436+
import contextvars
437437

438-
local = threading.local()
439-
if hasattr(local, '__decimal_context__'):
440-
del local.__decimal_context__
438+
_current_context_var = contextvars.ContextVar('decimal_context')
441439

442-
def getcontext(_local=local):
440+
def getcontext():
443441
"""Returns this thread's context.
444442
445443
If this thread does not yet have a context, returns
446444
a new context and sets this thread's context.
447445
New contexts are copies of DefaultContext.
448446
"""
449447
try:
450-
return _local.__decimal_context__
451-
except AttributeError:
448+
return _current_context_var.get()
449+
except LookupError:
452450
context = Context()
453-
_local.__decimal_context__ = context
451+
_current_context_var.set(context)
454452
return context
455453

456-
def setcontext(context, _local=local):
454+
def setcontext(context):
457455
"""Set this thread's context to context."""
458456
if context in (DefaultContext, BasicContext, ExtendedContext):
459457
context = context.copy()
460458
context.clear_flags()
461-
_local.__decimal_context__ = context
459+
_current_context_var.set(context)
462460

463-
del threading, local # Don't contaminate the namespace
461+
del contextvars # Don't contaminate the namespace
464462

465463
def localcontext(ctx=None):
466464
"""Return a context manager for a copy of the supplied context
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import asyncio
2+
import decimal
3+
import unittest
4+
5+
6+
class DecimalContextTest(unittest.TestCase):
7+
8+
def test_asyncio_task_decimal_context(self):
9+
async def fractions(t, precision, x, y):
10+
with decimal.localcontext() as ctx:
11+
ctx.prec = precision
12+
a = decimal.Decimal(x) / decimal.Decimal(y)
13+
await asyncio.sleep(t)
14+
b = decimal.Decimal(x) / decimal.Decimal(y ** 2)
15+
return a, b
16+
17+
async def main():
18+
r1, r2 = await asyncio.gather(
19+
fractions(0.1, 3, 1, 3), fractions(0.2, 6, 1, 3))
20+
21+
return r1, r2
22+
23+
r1, r2 = asyncio.run(main())
24+
25+
self.assertEqual(str(r1[0]), '0.333')
26+
self.assertEqual(str(r1[1]), '0.111')
27+
28+
self.assertEqual(str(r2[0]), '0.333333')
29+
self.assertEqual(str(r2[1]), '0.111111')
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Refactor decimal module to use contextvars to store decimal context.

‎Modules/_decimal/_decimal.c‎

Lines changed: 31 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -122,10 +122,7 @@ incr_false(void)
122122
}
123123

124124

125-
/* Key for thread state dictionary */
126-
static PyObject *tls_context_key = NULL;
127-
/* Invariant: NULL or the most recently accessed thread local context */
128-
static PyDecContextObject *cached_context = NULL;
125+
static PyContextVar *current_context_var;
129126

130127
/* Template for creating new thread contexts, calling Context() without
131128
* arguments and initializing the module_context on first access. */
@@ -1220,10 +1217,6 @@ context_new(PyTypeObject *type, PyObject *args UNUSED, PyObject *kwds UNUSED)
12201217
static void
12211218
context_dealloc(PyDecContextObject *self)
12221219
{
1223-
if (self == cached_context) {
1224-
cached_context = NULL;
1225-
}
1226-
12271220
Py_XDECREF(self->traps);
12281221
Py_XDECREF(self->flags);
12291222
Py_TYPE(self)->tp_free(self);
@@ -1498,117 +1491,61 @@ static PyGetSetDef context_getsets [] =
14981491
* operation.
14991492
*/
15001493

1501-
/* Get the context from the thread state dictionary. */
15021494
static PyObject *
1503-
current_context_from_dict(void)
1495+
init_current_context(void)
15041496
{
1505-
PyObject *dict;
1506-
PyObject *tl_context;
1507-
PyThreadState *tstate;
1508-
1509-
dict = PyThreadState_GetDict();
1510-
if (dict == NULL) {
1511-
PyErr_SetString(PyExc_RuntimeError,
1512-
"cannot get thread state");
1497+
PyObject *tl_context = context_copy(default_context_template, NULL);
1498+
if (tl_context == NULL) {
15131499
return NULL;
15141500
}
1501+
CTX(tl_context)->status = 0;
15151502

1516-
tl_context = PyDict_GetItemWithError(dict, tls_context_key);
1517-
if (tl_context != NULL) {
1518-
/* We already have a thread local context. */
1519-
CONTEXT_CHECK(tl_context);
1520-
}
1521-
else {
1522-
if (PyErr_Occurred()) {
1523-
return NULL;
1524-
}
1525-
1526-
/* Set up a new thread local context. */
1527-
tl_context = context_copy(default_context_template, NULL);
1528-
if (tl_context == NULL) {
1529-
return NULL;
1530-
}
1531-
CTX(tl_context)->status = 0;
1532-
1533-
if (PyDict_SetItem(dict, tls_context_key, tl_context) < 0) {
1534-
Py_DECREF(tl_context);
1535-
return NULL;
1536-
}
1503+
PyContextToken *tok = PyContextVar_Set(current_context_var, tl_context);
1504+
if (tok == NULL) {
15371505
Py_DECREF(tl_context);
1506+
return NULL;
15381507
}
1508+
Py_DECREF(tok);
15391509

1540-
/* Cache the context of the current thread, assuming that it
1541-
* will be accessed several times before a thread switch. */
1542-
tstate = PyThreadState_GET();
1543-
if (tstate) {
1544-
cached_context = (PyDecContextObject *)tl_context;
1545-
cached_context->tstate = tstate;
1546-
}
1547-
1548-
/* Borrowed reference with refcount==1 */
15491510
return tl_context;
15501511
}
15511512

1552-
/* Return borrowed reference to thread local context. */
1553-
static PyObject *
1513+
static inline PyObject *
15541514
current_context(void)
15551515
{
1556-
PyThreadState *tstate;
1516+
PyObject *tl_context;
1517+
if (PyContextVar_Get(current_context_var, NULL, &tl_context) < 0) {
1518+
return NULL;
1519+
}
15571520

1558-
tstate = PyThreadState_GET();
1559-
if (cached_context && cached_context->tstate == tstate) {
1560-
return (PyObject *)cached_context;
1521+
if (tl_context != NULL) {
1522+
return tl_context;
15611523
}
15621524

1563-
return current_context_from_dict();
1525+
return init_current_context();
15641526
}
15651527

15661528
/* ctxobj := borrowed reference to the current context */
15671529
#define CURRENT_CONTEXT(ctxobj) \
15681530
ctxobj = current_context(); \
15691531
if (ctxobj == NULL) { \
15701532
return NULL; \
1571-
}
1572-
1573-
/* ctx := pointer to the mpd_context_t struct of the current context */
1574-
#define CURRENT_CONTEXT_ADDR(ctx) { \
1575-
PyObject *_c_t_x_o_b_j = current_context(); \
1576-
if (_c_t_x_o_b_j == NULL) { \
1577-
return NULL; \
1578-
} \
1579-
ctx = CTX(_c_t_x_o_b_j); \
1580-
}
1533+
} \
1534+
Py_DECREF(ctxobj);
15811535

15821536
/* Return a new reference to the current context */
15831537
static PyObject *
15841538
PyDec_GetCurrentContext(PyObject *self UNUSED, PyObject *args UNUSED)
15851539
{
1586-
PyObject *context;
1587-
1588-
context = current_context();
1589-
if (context == NULL) {
1590-
return NULL;
1591-
}
1592-
1593-
Py_INCREF(context);
1594-
return context;
1540+
return current_context();
15951541
}
15961542

15971543
/* Set the thread local context to a new context, decrement old reference */
15981544
static PyObject *
15991545
PyDec_SetCurrentContext(PyObject *self UNUSED, PyObject *v)
16001546
{
1601-
PyObject *dict;
1602-
16031547
CONTEXT_CHECK(v);
16041548

1605-
dict = PyThreadState_GetDict();
1606-
if (dict == NULL) {
1607-
PyErr_SetString(PyExc_RuntimeError,
1608-
"cannot get thread state");
1609-
return NULL;
1610-
}
1611-
16121549
/* If the new context is one of the templates, make a copy.
16131550
* This is the current behavior of decimal.py. */
16141551
if (v == default_context_template ||
@@ -1624,13 +1561,13 @@ PyDec_SetCurrentContext(PyObject *self UNUSED, PyObject *v)
16241561
Py_INCREF(v);
16251562
}
16261563

1627-
cached_context = NULL;
1628-
if (PyDict_SetItem(dict, tls_context_key, v) < 0) {
1629-
Py_DECREF(v);
1564+
PyContextToken *tok = PyContextVar_Set(current_context_var, v);
1565+
Py_DECREF(v);
1566+
if (tok == NULL) {
16301567
return NULL;
16311568
}
1569+
Py_DECREF(tok);
16321570

1633-
Py_DECREF(v);
16341571
Py_RETURN_NONE;
16351572
}
16361573

@@ -4458,6 +4395,7 @@ _dec_hash(PyDecObject *v)
44584395
if (context == NULL) {
44594396
return -1;
44604397
}
4398+
Py_DECREF(context);
44614399

44624400
if (mpd_isspecial(MPD(v))) {
44634401
if (mpd_issnan(MPD(v))) {
@@ -5599,6 +5537,11 @@ PyInit__decimal(void)
55995537
mpd_free = PyMem_Free;
56005538
mpd_setminalloc(_Py_DEC_MINALLOC);
56015539

5540+
/* Init context variable */
5541+
current_context_var = PyContextVar_New("decimal_context", NULL);
5542+
if (current_context_var == NULL) {
5543+
goto error;
5544+
}
56025545

56035546
/* Init external C-API functions */
56045547
_py_long_multiply = PyLong_Type.tp_as_number->nb_multiply;
@@ -5768,7 +5711,6 @@ PyInit__decimal(void)
57685711
CHECK_INT(PyModule_AddObject(m, "DefaultContext",
57695712
default_context_template));
57705713

5771-
ASSIGN_PTR(tls_context_key, PyUnicode_FromString("___DECIMAL_CTX__"));
57725714
Py_INCREF(Py_True);
57735715
CHECK_INT(PyModule_AddObject(m, "HAVE_THREADS", Py_True));
57745716

@@ -5827,9 +5769,9 @@ PyInit__decimal(void)
58275769
Py_CLEAR(SignalTuple); /* GCOV_NOT_REACHED */
58285770
Py_CLEAR(DecimalTuple); /* GCOV_NOT_REACHED */
58295771
Py_CLEAR(default_context_template); /* GCOV_NOT_REACHED */
5830-
Py_CLEAR(tls_context_key); /* GCOV_NOT_REACHED */
58315772
Py_CLEAR(basic_context_template); /* GCOV_NOT_REACHED */
58325773
Py_CLEAR(extended_context_template); /* GCOV_NOT_REACHED */
5774+
Py_CLEAR(current_context_var); /* GCOV_NOT_REACHED */
58335775
Py_CLEAR(m); /* GCOV_NOT_REACHED */
58345776

58355777
return NULL; /* GCOV_NOT_REACHED */

0 commit comments

Comments
 (0)