Skip to content

Commit c02eb3e

Browse files
author
Anselm Kruis
committed
Stackless issue python#239: fix issues found during review
- better documentation of the concept - fix a left-over from the time, when setting the context of a main- tasklet was not supported - fix the context handling, when the main tasklet starts/ends. Copy the context from/to the thread state.
1 parent 266ee33 commit c02eb3e

File tree

5 files changed

+72
-16
lines changed

5 files changed

+72
-16
lines changed

Doc/library/stackless/tasklets.rst

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -609,27 +609,27 @@ Design requirements were
609609
4. Enable the integration of tasklet-based co-routines into the :mod:`asyncio` framework.
610610
This is an obvious application which involves context variables and tasklets.
611611

612-
Now each tasklet object has a private context attribute, which is either undefined (``NULL``) or a
613-
:class:`~contextvars.Context` object. The design goals have some consequences:
612+
Now each tasklet object has it own private context attribute. The design goals have some consequences:
614613

615614
* The active :class:`~contextvars.Context` object of a thread (as defined by the |PPL|)
616615
is the context of the :attr:`~stackless.current` tasklet. This implies that a tasklet switch,
617616
switches the active context of the thread.
618617

619618
* In accordance with the design decisions made in :pep:`567` the context of a tasklet can't be
620-
accessed directly, but you can use the method :meth:`tasklet.context_run` to run arbitrary code
619+
accessed directly [#f1]_, but you can use the method :meth:`tasklet.context_run` to run arbitrary code
621620
in this context. For instance ``tasklet.context_run(contextvars.copy_context())`` returns a copy
622621
of the context.
623622
The attribute :attr:`tasklet.context_id` can be used to test, if two tasklets share the context.
624623

625-
* A tasklet, whose context is undefined must behave identically to a tasklet, whose context is an
626-
empty :class:`~contextvars.Context` object. [#f1]_ Therefore the |PY| API provides no way to distinguish
627-
both states.
628-
629-
* Whenever the context of a tasklet is to be shared with another tasklet and the context is initially
630-
undefined, it must be set to a newly created :class:`~contextvars.Context` object beforehand.
624+
* If you use the C-API, the context attribute of a tasklet is stored in the field *context* of the structure
625+
:c:type:`PyTaskletObject` or :c:type:`PyThreadState`. This field is is either undefined (``NULL``) or a pointer to a
626+
:class:`~contextvars.Context` object.
627+
A tasklet, whose *context* is ``NULL`` **must** behave identically to a tasklet, whose context is an
628+
empty :class:`~contextvars.Context` object [#f2]_. Therefore the |PY| API provides no way to distinguish
629+
both states. Whenever the context of a tasklet is to be shared with another tasklet and `tasklet->context`
630+
is initially `NULL`, it must be set to a newly created :class:`~contextvars.Context` object beforehand.
631631
This affects the methods :meth:`~tasklet.context_run`, :meth:`~tasklet.__init__`, :meth:`~tasklet.bind`
632-
and :meth:`~tasklet.__setstate__`.
632+
and :meth:`~tasklet.__setstate__` as well as the attribute :attr:`tasklet.context_id`.
633633

634634
* If the state of a tasklet changes from *not alive* to *bound* or to *alive* (methods :meth:`~tasklet.__init__`,
635635
:meth:`~tasklet.bind` or :meth:`~tasklet.__setstate__`), the context
@@ -645,7 +645,11 @@ Now each tasklet object has a private context attribute, which is either undefin
645645

646646
.. rubric:: Footnotes
647647

648-
.. [#f1] Setting a context variable to a non default value sets a previously undefined
649-
context attribute to a newly created :class:`~contextvars.Context` object. This can happen anytime in a
648+
.. [#f1] Not exactly true. The return value of :meth:`tasklet.__reduce_ex__` can contain references to class
649+
:class:`contextvars.Context`, but it is strongly discouraged, to use them for any other purpose
650+
than pickling.
651+
652+
.. [#f2] Setting a context variable to a non default value changes the value of the field *context* from ``NULL``
653+
to a pointer to a newly created :class:`~contextvars.Context` object. This can happen anytime in a
650654
library call. Therefore any difference between an undefined context and an empty context causes ill defined
651655
behavior.

Include/slp_structs.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ typedef struct _tasklet {
134134
int recursion_depth;
135135
PyObject *def_globals;
136136
PyObject *tsk_weakreflist;
137-
PyObject *context; /* if running: the saved context, otherwise the context for the tasklet */
137+
/* If the tasklet is current: the context, the value of ts->context when the main tasklet was created.
138+
* (The context of a current tasklet is always ints->tasklet.)
139+
* If the tasklet is not current: the context for the tasklet */
140+
PyObject *context;
138141
} PyTaskletObject;
139142

140143

Stackless/module/scheduling.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,6 +1298,9 @@ slp_initialize_main_and_current(void)
12981298
assert(task->exc_state.exc_traceback == NULL);
12991299
assert(task->exc_state.previous_item == NULL);
13001300
assert(task->exc_info == &task->exc_state);
1301+
assert(task->context == NULL);
1302+
Py_XINCREF(ts->context);
1303+
task->context = ts->context;
13011304
SLP_EXCHANGE_EXCINFO(ts, task);
13021305

13031306
NOTIFY_SCHEDULE(ts, NULL, task, -1);
@@ -1393,6 +1396,9 @@ schedule_task_destruct(PyObject **retval, PyTaskletObject *prev, PyTaskletObject
13931396
assert(ts->exc_info == &prev->exc_state);
13941397
SLP_EXCHANGE_EXCINFO(ts, prev);
13951398
TASKLET_CLAIMVAL(prev, retval);
1399+
Py_XINCREF(prev->context);
1400+
Py_XSETREF(ts->context, prev->context);
1401+
ts->context_ver++;
13961402
if (PyBomb_Check(*retval))
13971403
*retval = slp_bomb_explode(*retval);
13981404
}

Stackless/module/taskletobject.c

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -434,10 +434,8 @@ PyTasklet_BindEx(PyTaskletObject *task, PyObject *func, PyObject *args, PyObject
434434

435435
/*
436436
* Set the context to the current context. It can be changed later on.
437-
* But only for non-main tasklest, because tasklet.set_context must not
438-
* be used for a main tasklet.
439437
*/
440-
if (func && !(ts && task == ts->st.main))
438+
if (func)
441439
if (_tasklet_init_context(task))
442440
return -1;
443441

Stackless/unittests/test_miscell.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,6 +1752,51 @@ def get_cid():
17521752
self.assertNotEqual(cid0, cid)
17531753
self.assertEqual(id(ctx), cid)
17541754

1755+
def test_main_tasklet_init(self):
1756+
# This test succeeds, if Stackless copies ts->context to into the main
1757+
# tasklet, when Stackless creates the main tasklet.
1758+
# This is important, if there is already a context set, when the interpreter
1759+
# gets called. Example: an interactive python prompt.
1760+
# See also: test_main_tasklet_fini
1761+
cid1 = stackless.current.context_id
1762+
cid2 = None
1763+
def task():
1764+
nonlocal cid2
1765+
cid2 = stackless.main.context_id
1766+
1767+
stackless.tasklet(task)()
1768+
stackless.test_outside()
1769+
self.assertEqual(stackless.current.context_id, cid1)
1770+
self.assertEqual(cid1, cid2)
1771+
1772+
def test_main_tasklet_fini(self):
1773+
# for a main tasklet of a thread initially ts->context == NULL
1774+
# This test succeeds, if Stackless copies the context of the main
1775+
# tasklet to ts->context after the main tasklet exits
1776+
# This way the last context of the main tasklet is preserved and available
1777+
# on the next invocation of the interpreter.
1778+
ctx_holder1 = None # use a tasklet to keep the context alive
1779+
ctx_holder2 = None
1780+
def task():
1781+
nonlocal ctx_holder1
1782+
ctx_holder1 = stackless.main.context_run(stackless.tasklet, id)
1783+
self.assertEqual(ctx_holder1.context_id, stackless.main.context_id)
1784+
1785+
t = stackless.tasklet(task, ())
1786+
def other_thread():
1787+
nonlocal ctx_holder2
1788+
t.bind_thread()
1789+
t.insert()
1790+
stackless.test_outside()
1791+
ctx_holder2 = stackless.tasklet(id)
1792+
self.assertEqual(ctx_holder2.context_id, stackless.current.context_id)
1793+
1794+
tr = threading.Thread(target=other_thread, name="other thread")
1795+
tr.start()
1796+
tr.join()
1797+
self.assertIsNot(ctx_holder1, ctx_holder2)
1798+
self.assertEqual(ctx_holder1.context_id, ctx_holder2.context_id)
1799+
17551800

17561801
#///////////////////////////////////////////////////////////////////////////////
17571802

0 commit comments

Comments
 (0)