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

Commit 032a566

Browse files
authored
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()
1 parent a11414b commit 032a566

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
@@ -761,6 +761,11 @@ PyTaskletTStateStruc * slp_get_saved_tstate(PyTaskletObject *task);
761761
PyObject * slp_channel_seq_callback(struct _frame *f, int throwflag, PyObject *retval);
762762
PyObject * slp_get_channel_callback(void);
763763

764+
/*
765+
* contextvars related prototypes
766+
*/
767+
PyObject* slp_context_run_callback(PyFrameObject *f, int exc, PyObject *result);
768+
764769
/* macro for use when interrupting tasklets from watchdog */
765770
#define TASKLET_NESTING_OK(task) \
766771
(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
@@ -25,6 +25,7 @@ def __reduce_ex__(*args):
2525
PICKLEFLAGS_PRESERVE_TRACING_STATE = 1
2626
PICKLEFLAGS_PRESERVE_AG_FINALIZER = 2
2727
PICKLEFLAGS_RESET_AG_FINALIZER = 4
28+
PICKLEFLAGS_PICKLE_CONTEXT = 8
2829

2930
# Backwards support for unpickling older pickles, even from 2.7
3031
from _stackless import _wrap
@@ -73,6 +74,37 @@ def __iter__(self):
7374
# expressions like "stackless.current" as well defined.
7475
current = runcount = main = debug = uncollectables = threads = pickle_with_tracing_state = None
7576

77+
def _tasklet_get_unpicklable_state(tasklet):
78+
"""Get a dict with additional state, that can't be pickled
79+
80+
The method tasklet.__reduce_ex__() returns the picklable state and this
81+
function returns a tuple containing the rest.
82+
83+
The items in the return value are:
84+
'context': the context of the tasklet
85+
86+
Additional items may be added later.
87+
88+
Note: this function has been added on a provisional basis (see :pep:`411` for details.)
89+
"""
90+
if not isinstance(tasklet, _stackless.tasklet):
91+
raise TypeError("Argument must be a tasklet")
92+
93+
with atomic():
94+
if tasklet.is_current or tasklet.thread_id != _stackless.current.thread_id:
95+
# - A current tasklet can't be reduced.
96+
# - We can't set pickle_flags for a foreign thread
97+
# To mitigate these problems, we copy the context to a new tasklet
98+
# (implicit copy by tasklet.__init__(callable, ...)) and reduce the new
99+
# context instead
100+
tasklet = tasklet.context_run(_stackless.tasklet, id) # "id" is just an arbitrary callable
101+
102+
flags = pickle_flags(PICKLEFLAGS_PICKLE_CONTEXT, PICKLEFLAGS_PICKLE_CONTEXT)
103+
try:
104+
return {'context': tasklet.__reduce__()[2][8]}
105+
finally:
106+
pickle_flags(flags, PICKLEFLAGS_PICKLE_CONTEXT)
107+
76108
def transmogrify():
77109
"""
78110
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
@@ -604,23 +604,32 @@ _contextvars_Context_copy_impl(PyContext *self)
604604

605605

606606
#ifdef STACKLESS
607-
static PyObject* context_run_callback(PyFrameObject *f, int exc, PyObject *result)
607+
PyObject* slp_context_run_callback(PyFrameObject *f, int exc, PyObject *result)
608608
{
609609
PyCFrameObject *cf = (PyCFrameObject *)f;
610-
assert(PyContext_CheckExact(cf->ob1));
611610
PyObject *context = cf->ob1;
612611
cf->ob1 = NULL;
613612

614-
if (PyContext_Exit(context)) {
615-
Py_CLEAR(result);
613+
if (cf->i) {
614+
/* called by tasklet.context_run(...) */
615+
PyThreadState *ts = PyThreadState_GET();
616+
assert(ts);
617+
assert(NULL == context || PyContext_CheckExact(context));
618+
Py_XSETREF(ts->context, context);
619+
ts->context_ver++;
620+
} else {
621+
assert(PyContext_CheckExact(context));
622+
if (PyContext_Exit(context)) {
623+
Py_CLEAR(result);
624+
}
625+
Py_DECREF(context);
616626
}
617627

618-
Py_DECREF(context);
619628
SLP_STORE_NEXT_FRAME(PyThreadState_GET(), cf->f_back);
620629
return result;
621630
}
622631

623-
SLP_DEF_INVALID_EXEC(context_run_callback)
632+
SLP_DEF_INVALID_EXEC(slp_context_run_callback)
624633
#endif
625634

626635

@@ -629,6 +638,7 @@ context_run(PyContext *self, PyObject *const *args,
629638
Py_ssize_t nargs, PyObject *kwnames)
630639
{
631640
STACKLESS_GETARG();
641+
assert(NULL != self);
632642

633643
if (nargs < 1) {
634644
PyErr_SetString(PyExc_TypeError,
@@ -644,11 +654,12 @@ context_run(PyContext *self, PyObject *const *args,
644654
PyThreadState *ts = PyThreadState_GET();
645655
PyCFrameObject *f = NULL;
646656
if (stackless) {
647-
f = slp_cframe_new(context_run_callback, 1);
657+
f = slp_cframe_new(slp_context_run_callback, 1);
648658
if (f == NULL)
649659
return NULL;
650660
Py_INCREF(self);
651661
f->ob1 = (PyObject *)self;
662+
assert(f->i == 0);
652663
SLP_SET_CURRENT_FRAME(ts, (PyFrameObject *)f);
653664
/* f contains the only counted reference to current frame. This reference
654665
* keeps the fame alive during the following _PyObject_FastCallKeywords().
@@ -1326,7 +1337,7 @@ _PyContext_Init(void)
13261337

13271338
#ifdef STACKLESS
13281339
if (slp_register_execute(&PyCFrame_Type, "context_run_callback",
1329-
context_run_callback, SLP_REF_INVALID_EXEC(context_run_callback)) != 0)
1340+
slp_context_run_callback, SLP_REF_INVALID_EXEC(slp_context_run_callback)) != 0)
13301341
{
13311342
return 0;
13321343
}

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/245
1323
Prevent a crash, if you call tasklet.__init__() or tasklet.bind() with wrong
1424
argument types.

0 commit comments

Comments
 (0)