Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit a599283

Browse files
akruisAnselm Kruis
authored and
Anselm Kruis
committed
Stackless issue #239: add support for PEP 567 context variables
Add a private context attribute and appropriate methods to class tasklet. Document the changes in the manual. New methods: tasklet.set_context(context), tasklet.context_run(...) New readonly attribute: tasklet.context_id Add/improve pickling of the context of tasklets. New pickle flag "PICKLEFLAGS_PICKLE_CONTEXT", new undocumented function stackless._tasklet_get_unpicklable_state() (cherry picked from commit 032a566)
1 parent f9cbc8b commit a599283

File tree

16 files changed

+1340
-32
lines changed

16 files changed

+1340
-32
lines changed

Doc/library/stackless/pickling.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,25 @@ different address than *t1*, which was displayed earlier.
108108
objects and frame objects contain code objects. And code objects are
109109
usually incompatible between different minor versions of |CPY|.
110110

111+
.. note::
112+
113+
If you pickle a tasklet, its :class:`~contextvars.Context` won't be pickled,
114+
because :class:`~contextvars.Context` objects can't be pickled. See
115+
:pep:`567` for an explanation.
116+
117+
It is sometimes possible enable pickling of :class:`~contextvars.Context` objects
118+
in an application specific way (see for instance: :func:`copyreg.pickle` or
119+
:attr:`pickle.Pickler.dispatch_table` or :attr:`pickle.Pickler.persistent_id`).
120+
Such an application can set the pickle flag
121+
:const:`~stackless.PICKLEFLAGS_PICKLE_CONTEXT` to include the
122+
context in the pickled state of a tasklet.
123+
124+
Another option is to subclass :class:`tasklet` and overload the methods
125+
:meth:`tasklet.__reduce_ex__` and :meth:`tasklet.__setstate__` to
126+
pickle the values of particular :class:`~contextvars.ContextVar` objects together
127+
with the tasklet.
128+
129+
111130
======================
112131
Pickling other objects
113132
======================

Doc/library/stackless/stackless.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,21 @@ Constants
5151
These constants have been added on a provisional basis (see :pep:`411`
5252
for details.)
5353

54+
.. data:: PICKLEFLAGS_PICKLE_CONTEXT
55+
56+
This constant defines an option flag for the function
57+
:func:`pickle_flags`.
58+
59+
If this flag is set, |SLP| assumes that a :class:`~contextvars.Context` object
60+
is pickleable. As a consequence the state information returned by :meth:`tasklet.__reduce_ex__`
61+
includes the context of the tasklet.
62+
63+
.. versionadded:: 3.7.6
64+
65+
.. note::
66+
This constant has been added on a provisional basis (see :pep:`411`
67+
for details.)
68+
5469
---------
5570
Functions
5671
---------

Doc/library/stackless/tasklets.rst

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ The ``tasklet`` class
110110
:meth:`tasklet.setup`. The difference is that when providing them to
111111
:meth:`tasklet.bind`, the tasklet is not made runnable yet.
112112

113+
.. versionadded:: 3.7.6
114+
115+
If *func* is not :data:`None`, this method also sets the
116+
:class:`~contextvars.Context` object of this tasklet to the
117+
:class:`~contextvars.Context` object of the current tasklet.
118+
Therefore it is usually not required to set the context explicitly.
119+
113120
*func* can be :data:`None` when providing arguments, in which case a previous call
114121
to :meth:`tasklet.bind` must have provided the function.
115122

@@ -344,6 +351,61 @@ The ``tasklet`` class
344351
# Implement unsafe logic here.
345352
t.set_ignore_nesting(old_value)
346353

354+
.. method:: tasklet.set_context(context)
355+
356+
.. versionadded:: 3.7.6
357+
358+
Set the :class:`~contextvars.Context` object to be used while this tasklet runs.
359+
360+
Every tasklet has a private context attribute.
361+
When the tasklet runs, this context becomes the current context of the thread.
362+
363+
:param context: the context to be set
364+
:type context: :class:`contextvars.Context`
365+
:return: the tasklet itself
366+
:rtype: :class:`tasklet`
367+
:raises RuntimeError: if the tasklet is bound to a foreign thread and is current or scheduled.
368+
:raises RuntimeError: if called from within :meth:`contextvars.Context.run`.
369+
370+
.. note::
371+
372+
The methods :meth:`__init__`, :meth:`bind` and :meth:`__setstate__` also set the context
373+
of the tasklet they are called on to the context of the current tasklet. Therefore it is
374+
usually not required to set the context explicitly.
375+
376+
.. note::
377+
This method has been added on a provisional basis (see :pep:`411`
378+
for details.)
379+
380+
.. method:: tasklet.context_run(callable, \*args, \*\*kwargs)
381+
382+
.. versionadded:: 3.7.6
383+
384+
Execute ``callable(*args, **kwargs)`` in the context object of the tasklet
385+
the contest_run method is called on. Return the result of the
386+
execution or propagate an exception if one occurred.
387+
This method is roughly equivalent following pseudo code::
388+
389+
def context_run(self, callable, *args, **kwargs):
390+
saved_context = stackless.current._internal_get_context()
391+
stackless.current.set_context(self._internal_get_context())
392+
try:
393+
return callable(*args, **kw)
394+
finally:
395+
stackless.current.set_context(saved_context)
396+
397+
See also :meth:`contextvars.Context.run` for additional information.
398+
Use this method with care, because it lets you manipulate the context of
399+
another tasklet. Often it is sufficient to use a copy of the context
400+
instead of the original object::
401+
402+
copied_context = tasklet.context_run(contextvars.copy_context)
403+
copied_context.run(...)
404+
405+
.. note::
406+
This method has been added on a provisional basis (see :pep:`411`
407+
for details.)
408+
347409
.. method:: tasklet.__del__()
348410

349411
.. versionadded:: 3.7
@@ -365,6 +427,13 @@ The ``tasklet`` class
365427

366428
See :meth:`object.__setstate__`.
367429

430+
.. versionadded:: 3.7.6
431+
432+
If the tasklet becomes alive through this call and if *state* does not contain
433+
a :class:`~contextvars.Context` object, then :meth:`~__setstate__` also sets
434+
the :class:`~contextvars.Context` object of the
435+
tasklet to the :class:`~contextvars.Context` object of the current tasklet.
436+
368437
:param state: the state as given by ``__reduce_ex__(...)[2]``
369438
:type state: :class:`tuple`
370439
:return: self
@@ -423,6 +492,18 @@ The following attributes allow checking of user set situations:
423492
This attribute is ``True`` while this tasklet is within a
424493
:meth:`tasklet.set_ignore_nesting` block
425494

495+
.. attribute:: tasklet.context_id
496+
497+
.. versionadded:: 3.7.6
498+
499+
This attribute is the :func:`id` of the :class:`~contextvars.Context` object to be used while this tasklet runs.
500+
It is intended mostly for debugging.
501+
502+
.. note::
503+
This attribute has been added on a provisional basis (see :pep:`411`
504+
for details.)
505+
506+
426507
The following attributes allow identification of tasklet place:
427508

428509
.. attribute:: tasklet.is_current
@@ -511,3 +592,72 @@ state transitions these functions are roughly equivalent to the following
511592

512593
def schedule_remove():
513594
stackless.current.next.switch()
595+
596+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
597+
Tasklets and Context Variables
598+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
599+
600+
.. versionadded:: 3.7.6
601+
602+
Version 3.7 of the |PPL| adds context variables, see module :mod:`contextvars`.
603+
Usually they are used in connection with
604+
:mod:`asyncio`, but they are a useful concept for |SLP| too.
605+
Using context variables and multiple tasklets together didn't work well in |SLP| versions 3.7.0 to
606+
3.7.5, because all tasklets of a given thread shared the same context.
607+
608+
Starting with version 3.7.6 |SLP| adds explicit support for context variables.
609+
Design requirements were:
610+
611+
1. Be fully compatible with |CPY| and its design decisions.
612+
2. Be fully compatible with previous applications of |SLP|, which are unaware of context variables.
613+
3. Automatically share a context between related tasklets. This way a tasklet, that needs to set
614+
a context variable, can delegate this duty to a sub-tasklet without the need to manage the
615+
context of the sub-tasklet manually.
616+
4. Enable the integration of tasklet-based co-routines into the :mod:`asyncio` framework.
617+
This is an obvious application which involves context variables and tasklets. See
618+
`slp-coroutine <https://pypi.org/project/slp-coroutine>`_ for an example.
619+
620+
Now each tasklet object has it own private context attribute. The design goals have some consequences:
621+
622+
* The active :class:`~contextvars.Context` object of a thread (as defined by the |PPL|)
623+
is the context of the :attr:`~stackless.current` tasklet. This implies that a tasklet switch,
624+
switches the active context of the thread.
625+
626+
* In accordance with the design decisions made in :pep:`567` the context of a tasklet can't be
627+
accessed directly [#f1]_, but you can use the method :meth:`tasklet.context_run` to run arbitrary code
628+
in this context. For instance ``tasklet.context_run(contextvars.copy_context())`` returns a copy
629+
of the context.
630+
The attribute :attr:`tasklet.context_id` can be used to test, if two tasklets share the context.
631+
632+
* If you use the C-API, the context attribute of a tasklet is stored in the field *context* of the structure
633+
:c:type:`PyTaskletObject` or :c:type:`PyThreadState`. This field is is either undefined (``NULL``) or a pointer to a
634+
:class:`~contextvars.Context` object.
635+
A tasklet, whose *context* is ``NULL`` **must** behave identically to a tasklet, whose context is an
636+
empty :class:`~contextvars.Context` object [#f2]_. Therefore the |PY| API provides no way to distinguish
637+
both states. Whenever the context of a tasklet is to be shared with another tasklet and `tasklet->context`
638+
is initially `NULL`, it must be set to a newly created :class:`~contextvars.Context` object beforehand.
639+
This affects the methods :meth:`~tasklet.context_run`, :meth:`~tasklet.__init__`, :meth:`~tasklet.bind`
640+
and :meth:`~tasklet.__setstate__` as well as the attribute :attr:`tasklet.context_id`.
641+
642+
* If the state of a tasklet changes from *not alive* to *bound* or to *alive* (methods :meth:`~tasklet.__init__`,
643+
:meth:`~tasklet.bind` or :meth:`~tasklet.__setstate__`), the context
644+
of the tasklet is set to the currently active context. This way a newly initialized tasklet automatically
645+
shares the context of its creator.
646+
647+
* The :mod:`contextvars` implementation of |CPY| imposes several restrictions on |SLP|. Especially the sanity checks in
648+
:c:func:`PyContext_Enter` and :c:func:`PyContext_Exit` make it impossible to replace the current context within
649+
the execution of the method :meth:`contextvars.Context.run`. In that case |SLP| raises :exc:`RuntimeError`.
650+
651+
.. note::
652+
Context support has been added on a provisional basis (see :pep:`411` for details.)
653+
654+
.. rubric:: Footnotes
655+
656+
.. [#f1] Not exactly true. The return value of :meth:`tasklet.__reduce_ex__` can contain references to class
657+
:class:`contextvars.Context`, but it is strongly discouraged, to use them for any other purpose
658+
than pickling.
659+
660+
.. [#f2] Setting a context variable to a non default value changes the value of the field *context* from ``NULL``
661+
to a pointer to a newly created :class:`~contextvars.Context` object. This can happen anytime in a
662+
library call. Therefore any difference between an undefined context and an empty context causes ill defined
663+
behavior.

Include/internal/slp_prickelpit.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ Py_ssize_t slp_from_tuple_with_nulls(PyObject **start, PyObject *tup);
4949
#define SLP_PICKLEFLAGS_PRESERVE_TRACING_STATE (1U)
5050
#define SLP_PICKLEFLAGS_PRESERVE_AG_FINALIZER (1U<<1)
5151
#define SLP_PICKLEFLAGS_RESET_AG_FINALIZER (1U<<2)
52-
#define SLP_PICKLEFLAGS__MAX_VALUE ((1<<3)-1) /* must be a signed value */
52+
#define SLP_PICKLEFLAGS_PICKLE_CONTEXT (1U<<3)
53+
#define SLP_PICKLEFLAGS__MAX_VALUE ((1<<4)-1) /* must be a signed value */
5354

5455
/* helper functions for module dicts */
5556

Include/internal/stackless_impl.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,11 @@ PyTaskletTStateStruc * slp_get_saved_tstate(PyTaskletObject *task);
773773
PyObject * slp_channel_seq_callback(struct _frame *f, int throwflag, PyObject *retval);
774774
PyObject * slp_get_channel_callback(void);
775775

776+
/*
777+
* contextvars related prototypes
778+
*/
779+
PyObject* slp_context_run_callback(PyFrameObject *f, int exc, PyObject *result);
780+
776781
/* macro for use when interrupting tasklets from watchdog */
777782
#define TASKLET_NESTING_OK(task) \
778783
(ts->st.nesting_level == 0 || \

Include/slp_structs.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,9 @@ typedef struct _tasklet {
134134
int recursion_depth;
135135
PyObject *def_globals;
136136
PyObject *tsk_weakreflist;
137+
/* If the tasklet is current: NULL. (The context of a current tasklet is always in ts->tasklet.)
138+
* If the tasklet is not current: the context for the tasklet */
139+
PyObject *context;
137140
} PyTaskletObject;
138141

139142

Lib/stackless.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ def __reduce_ex__(*args):
1616
PICKLEFLAGS_PRESERVE_TRACING_STATE = 1
1717
PICKLEFLAGS_PRESERVE_AG_FINALIZER = 2
1818
PICKLEFLAGS_RESET_AG_FINALIZER = 4
19+
PICKLEFLAGS_PICKLE_CONTEXT = 8
1920

2021
# Backwards support for unpickling older pickles, even from 2.7
2122
from _stackless import _wrap
@@ -64,6 +65,37 @@ def __iter__(self):
6465
# expressions like "stackless.current" as well defined.
6566
current = runcount = main = debug = uncollectables = threads = pickle_with_tracing_state = None
6667

68+
def _tasklet_get_unpicklable_state(tasklet):
69+
"""Get a dict with additional state, that can't be pickled
70+
71+
The method tasklet.__reduce_ex__() returns the picklable state and this
72+
function returns a tuple containing the rest.
73+
74+
The items in the return value are:
75+
'context': the context of the tasklet
76+
77+
Additional items may be added later.
78+
79+
Note: this function has been added on a provisional basis (see :pep:`411` for details.)
80+
"""
81+
if not isinstance(tasklet, _stackless.tasklet):
82+
raise TypeError("Argument must be a tasklet")
83+
84+
with atomic():
85+
if tasklet.is_current or tasklet.thread_id != _stackless.current.thread_id:
86+
# - A current tasklet can't be reduced.
87+
# - We can't set pickle_flags for a foreign thread
88+
# To mitigate these problems, we copy the context to a new tasklet
89+
# (implicit copy by tasklet.__init__(callable, ...)) and reduce the new
90+
# context instead
91+
tasklet = tasklet.context_run(_stackless.tasklet, id) # "id" is just an arbitrary callable
92+
93+
flags = pickle_flags(PICKLEFLAGS_PICKLE_CONTEXT, PICKLEFLAGS_PICKLE_CONTEXT)
94+
try:
95+
return {'context': tasklet.__reduce__()[2][8]}
96+
finally:
97+
pickle_flags(flags, PICKLEFLAGS_PICKLE_CONTEXT)
98+
6799
def transmogrify():
68100
"""
69101
this function creates a subclass of the ModuleType with properties.

Python/context.c

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -619,23 +619,32 @@ _contextvars_Context_copy_impl(PyContext *self)
619619

620620

621621
#ifdef STACKLESS
622-
static PyObject* context_run_callback(PyFrameObject *f, int exc, PyObject *result)
622+
PyObject* slp_context_run_callback(PyFrameObject *f, int exc, PyObject *result)
623623
{
624624
PyCFrameObject *cf = (PyCFrameObject *)f;
625-
assert(PyContext_CheckExact(cf->ob1));
626625
PyObject *context = cf->ob1;
627626
cf->ob1 = NULL;
628627

629-
if (PyContext_Exit(context)) {
630-
Py_CLEAR(result);
628+
if (cf->i) {
629+
/* called by tasklet.context_run(...) */
630+
PyThreadState *ts = PyThreadState_GET();
631+
assert(ts);
632+
assert(NULL == context || PyContext_CheckExact(context));
633+
Py_XSETREF(ts->context, context);
634+
ts->context_ver++;
635+
} else {
636+
assert(PyContext_CheckExact(context));
637+
if (PyContext_Exit(context)) {
638+
Py_CLEAR(result);
639+
}
640+
Py_DECREF(context);
631641
}
632642

633-
Py_DECREF(context);
634643
SLP_STORE_NEXT_FRAME(PyThreadState_GET(), cf->f_back);
635644
return result;
636645
}
637646

638-
SLP_DEF_INVALID_EXEC(context_run_callback)
647+
SLP_DEF_INVALID_EXEC(slp_context_run_callback)
639648
#endif
640649

641650

@@ -644,6 +653,7 @@ context_run(PyContext *self, PyObject *const *args,
644653
Py_ssize_t nargs, PyObject *kwnames)
645654
{
646655
STACKLESS_GETARG();
656+
assert(NULL != self);
647657

648658
if (nargs < 1) {
649659
PyErr_SetString(PyExc_TypeError,
@@ -659,11 +669,12 @@ context_run(PyContext *self, PyObject *const *args,
659669
PyThreadState *ts = PyThreadState_GET();
660670
PyCFrameObject *f = NULL;
661671
if (stackless) {
662-
f = slp_cframe_new(context_run_callback, 1);
672+
f = slp_cframe_new(slp_context_run_callback, 1);
663673
if (f == NULL)
664674
return NULL;
665675
Py_INCREF(self);
666676
f->ob1 = (PyObject *)self;
677+
assert(f->i == 0);
667678
SLP_SET_CURRENT_FRAME(ts, (PyFrameObject *)f);
668679
/* f contains the only counted reference to current frame. This reference
669680
* keeps the fame alive during the following _PyObject_FastCallKeywords().
@@ -1361,7 +1372,7 @@ _PyContext_Init(void)
13611372

13621373
#ifdef STACKLESS
13631374
if (slp_register_execute(&PyCFrame_Type, "context_run_callback",
1364-
context_run_callback, SLP_REF_INVALID_EXEC(context_run_callback)) != 0)
1375+
slp_context_run_callback, SLP_REF_INVALID_EXEC(slp_context_run_callback)) != 0)
13651376
{
13661377
return 0;
13671378
}

Stackless/changelog.txt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@ What's New in Stackless 3.X.X?
99

1010
*Release date: 20XX-XX-XX*
1111

12+
- https://github.com/stackless-dev/stackless/issues/239
13+
Add support for PEP 567 context variables to tasklets.
14+
Each tasklet now has a contextvars.Context object, that becomes active during
15+
the execution of the tasklet.
16+
New tasklet methods "set_context()" and "context_run()".
17+
New read only attribute "tasklet.context_id".
18+
New constant "stackless.PICKLEFLAGS_PICKLE_CONTEXT".
19+
And an intentionally undocumented function
20+
"stackless._tasklet_get_unpicklable_state()"
21+
1222
- https://github.com/stackless-dev/stackless/issues/253
1323
Disable the support for Cython versions below 0.29.
1424
Starting with Stackless version 3.8.0b1 extension modules compiled with

0 commit comments

Comments
 (0)