
This is still rather rough, but I figured it's easier to let everybody fill in the remaining gaps by arguments than it is for me to pick a position I like and try to convince everybody else that it's right. :) Your feedback is requested and welcome. PEP: XXX Title: Task-local Variables Author: Phillip J. Eby <pje@telecommunity.com> Status: Draft Type: Standards Track Content-Type: text/x-rst Created: 19-Oct-2005 Python-Version: 2.5 Post-History: 19-Oct-2005 Abstract ======== Many Python modules provide some kind of global or thread-local state, which is relatively easy to implement. With the acceptance of PEP 342, however, co-routines will become more common, and it will be desirable in many cases to treat each as its own logical thread of execution. So, many kinds of state that might now be kept as a thread-specific variable (such as the "current transaction" in ZODB or the "current database connection" in SQLObject) will not work with coroutines. This PEP proposes a simple mechanism akin to thread-local variables, but which will make it easy and efficient for co-routine schedulers to switch state between tasks. The mechanism is proposed for the standard library because its usefulness is dependent on its adoption by standard library modules, such as the ``decimal`` module. The proposed features can be implemented as pure Python code, and as such are suitable for use by other Python implementations (including older versions of Python, if desired). Motivation ========== PEP 343's new "with" statement makes it very attractive to temporarily alter some aspect of system state, and then restore it, using a context manager. Many of PEP 343's examples are of this nature, whether they are temporarily redirecting ``sys.stdout``, or temporarily altering decimal precision. But when this attractive feature is combined with PEP 342-style co-routines, a new challenge emerges. Consider this code, which may misbehave if run as a co-routine:: with opening(filename, "w") as f: with redirecting_stdout(f): print "Hello world" yield pause(5) print "Goodbye world" Problems can arise from this code in two ways. First, the redirection of output "leaks out" to other coroutines during the pause. Second, when this coroutine is finished, it resets stdout to whatever it was at the beginning of the coroutine, regardless of what another co-routine might have been using. Similar issues can be demonstrated using the decimal context, transactions, database connections, etc., which are all likely to be popular contexts for the "with" statement. However, if these new context managers are written to use global or thread-local state, coroutines will be locked out of the market, so to speak. Therefore, this PEP proposes to provide and promote a standard way of managing per-execution-context state, such that coroutine schedulers can keep each coroutine's state distinct. If this mechanism is then used by library modules (such as ``decimal``) to maintain their current state, then they will be transparently compatible with co-routines as well as threaded and threadless code. (Note that for Python 2.x versions, backward compatibility requires that we continue to allow direct reassignment to e.g. ``sys.stdout``. So, it will still of course be possible to write code that will interoperate poorly with co-routines. But for Python 3.x it seems worth considering making some of the ``sys`` module's contents into task-local variables rather than assignment targets.) Specification ============= This PEP proposes to offer a standard library module called ``context``, with the following core contents: Variable A class that allows creation of a context variable (see below). snapshot() Returns a snapshot of the current execution context. swap(ctx) Set the current context to `ctx`, returning a snapshot of the current context. The basic idea here is that a co-routine scheduler can switch between tasks by doing something like:: last_coroutine.state = context.swap(next_coroutine.state) Or perhaps more like:: # ... execute coroutine iteration last_coroutine.state = context.snapshot() # ... figure out what routine to run next context.swap(next_coroutine.state) Each ``context.Variable`` stores and retrieves its state using the current execution context, which is thread-specific. (Thus, each thread may execute any number of concurrent tasks, although most practical systems today have only one thread that executes coroutines, the other threads being reserved for operations that would otherwise block co-routine execution. Nonetheless, such other threads will often still require context variables of their own.) Context Variable Objects ------------------------ A context variable object provides the following methods: get(default=None) Return the value of the variable in the current execution context, or `default` if not set. set(value) Set the value of the variable for the current execution context. unset() Delete the value of the variable for the current execution context. __call__(*value) If called with an argument, return a context manager that sets the variable to the specified value, then restores the old value upon ``__exit__``. If called without an argument, return the value of the variable for the current execution context, or raise an error if no value is set. Thus:: with some_variable(value): foo() would be roughly equivalent to:: old = some_variable() some_variable.set(value) try: foo() finally: some_variable.set(old) Implementation Details ---------------------- The simplest possible implementation is for ``Variable`` objects to use themselves as unique keys into an execution context dictionary. The context dictionary would be stored in another dictionary, keyed by ``get_thread_ident()``. This approach would work with almost any version or implementation of Python. For efficiency's sake, however, CPython could simply store the execution context dictionary in its "thread state" structure, creating an empty dictionary at thread initialization time. This would make it somewhat easier to offer a C API for access to context variables, especially where efficiency of access is desirable. But the proposal does not depend on this. In the PEP author's experiments, a simple copy-on-write optimization to the the ``set()`` and ``unset()`` methods allows for high performance task switching. By placing a "frozen" flag in the context dictionary when a snapshot is taken, and then checking for the flag before making changes, a single snapshot can be shared by multiple callers, and thus a ``swap()`` operation is little more than two dictionary writes and a read. This leads to higher performance in the typical case, because context variables are more likely to set in outer loops, but task switches are more likely to occur in inner loops. A copy-on-write approach thus prevents copying from occurring during most task switches. Possible Enhancements --------------------- The core of this proposal is extremely minimalist, as it should be possible to do almost anything desired using combinations of ``Variable`` objects or by simply using variables whose values are mutable objects. There are, however, a variety of options for enhancement: ``manager`` decorator The ``context`` module could perhaps be the home of the PEP 343 ``contextmanager`` decorator, effectively renamed to ``context.manager``. This could be a natural fit, in that it would remind the creators of new context managers that they should consider tracking any associated state in a ``context.Variable``. Proxy class Sometimes it's useful to have an object that looks like a module global (e.g. ``sys.stdout``) but which actually delegates its behavior to a context-specific instance. Thus, you could have one ``sys.stdout``, but its actual output would be directed based on the current execution context. The simplest form of such a proxy class might look something like:: class Proxy(object): def __init__(self, initial_value): self.var = context.Variable() self.var.set(initial_value) def __call__(self,*value): return object.__getattribute__(self,'var')(*value) def __getattribute__(self, attr): var = object.__getattribute__(self,'var') return getattr(var, attr) sys.stdout = Proxy(sys.stdout) # make sys.stdout selectable with sys.stdout(somefile): # temporary redirect in current context print "hey!" The main open issues in implementing this sort of proxy are in the precise set of special methods (e.g. ``__getitem__``, ``__setattr__``, etc.) that should be supported, and what API should be supplied for changing the value, setting a default value for new threads, etc. Low-level API Currently, this PEP does not specify an API for accessing and modifying the current execution context, nor a C API for such access. It currently assumes that ``snapshot()``, ``swap()`` and ``Variable`` are the only public means of accessing context information. It may be desirable to offer finer-grained APIs for use by more advanced uses (such as creating an API for management of proxies). And it may be desirable to have a C API for use by Python extensions that wish convenient access to context variables. Rationale ========= Different libraries have different uses for maintaining a "current" state, be it global or local to a specific thread or task. There is currently no way for task-management code to find and switch all of these "current" states. And even if it could, task switching performance would degrade linearly as new libraries were added. One possible alternative approach to this proposal, would be for explicit task objects to exist, and to provide a way to give them identities, so that libraries could instead store their own state as a property of the task, rather than storing their state in a task-specific mapping. This offers similar potential performance to a copy-on-write strategy, but would use more memory than this proposal when only one task is involved. (Because each variable would have a dictionary mapping from task to the variable's value, but in this proposal there is simply a single dictionary for the task.) Some languages offer "dynamically scoped" variables that are somewhat similar in behavior to the context variables proposed by this PEP. The principal differences are that: 1. Context variables are objects used to obtain or save a value, rather than being a syntactic construct of the language. 2. PEP 343 allows for *controlled* manipulation of context variables, helping to prevent "duelling libraries" from changing state on each other. Also, a library can potentially ``snapshot()`` a desired state at startup, and use ``swap()`` to restore that state on re-entry. (And could even define a simple decorator to wrap its entry points to ensure this.) 3. The PEP author is not aware of any language that explicitly offers coroutine-scoped variables, but presumes that they can be modelled with monads or continuations in functional languages like Haskell. (And I only mention this to forestall the otherwise-inevitable response from fans of such techniques, pointing out that it's possible.) Reference Implementation ======================== The author has prototyped an implementation with somewhat fancier features than shown here, but prefers not to publish it until the basic features and choices of optional functionality have been discussed on Python-Dev. Copyright ========= This document has been placed in the public domain. .. Local Variables: mode: indented-text indent-tabs-mode: nil sentence-end-double-space: t fill-column: 70 End:

"Phillip J. Eby" <pje@telecommunity.com> wrote:
For efficiency's sake, however, CPython could simply store the execution context dictionary in its "thread state" structure, creating an empty dictionary at thread initialization time. This would make it somewhat easier to offer a C API for access to context variables, especially where efficiency of access is desirable. But the proposal does not depend on this.
What about a situation in which corutines are handled by multiple threads? Any time a corutine passed from one thread to another, it would lose its state. While I agree with the obvious "don't do that" response, I don't believe that the proposal will actually go very far in preventing real problems when using context managers and generators or corutines. Why? How much task state is going to be monitored/saved? Just sys? Perhaps sys and the module in which a corutine was defined? Eventually you will have someone who says, "I need Python to be saving and restoring the state of the entire interpreter so that I can have a per-user execution environment that cannot be corrupted by another user." But how much farther out is that? - Josiah

At 07:30 PM 10/19/2005 -0700, Josiah Carlson wrote:
What about a situation in which corutines are handled by multiple threads? Any time a corutine passed from one thread to another, it would lose its state.
It's the responsibility of a coroutine scheduler to take a snapshot() when a task is suspended, and to swap() it in when resumed. So it doesn't matter that you've changed what thread you're running in, as long as you keep the context with the coroutine that "owns" it.
While I agree with the obvious "don't do that" response, I don't believe that the proposal will actually go very far in preventing real problems when using context managers and generators or corutines. Why? How much task state is going to be monitored/saved? Just sys? Perhaps sys and the module in which a corutine was defined?
As I mentioned in the PEP, I don't think that we would bother having Python-defined variables be context-specific until Python 3.0. This is mainly intended for the kinds of things described in the proposal: ZODB current transaction, current database connection, decimal context, etc. Basically, anything that you'd have a thread-local for now, and indeed most anything that you'd use a global variable and 'with:' for.
Eventually you will have someone who says, "I need Python to be saving and restoring the state of the entire interpreter so that I can have a per-user execution environment that cannot be corrupted by another user." But how much farther out is that?
I don't see how that's even related. This is simply a replacement for thread-local variables that allows you to also be compatible with "lightweight" (coroutine-based) threads.

"Phillip J. Eby" <pje@telecommunity.com> wrote:
It's the responsibility of a coroutine scheduler to take a snapshot() when a task is suspended, and to swap() it in when resumed. So it doesn't matter that you've changed what thread you're running in, as long as you keep the context with the coroutine that "owns" it.
As I mentioned in the PEP, I don't think that we would bother having Python-defined variables be context-specific until Python 3.0. This is mainly intended for the kinds of things described in the proposal: ZODB current transaction, current database connection, decimal context, etc. Basically, anything that you'd have a thread-local for now, and indeed most anything that you'd use a global variable and 'with:' for.
I don't see how that's even related. This is simply a replacement for thread-local variables that allows you to also be compatible with "lightweight" (coroutine-based) threads.
I just re-read the proposal with your clarifications in mind. Looks good. +1 - Josiah

Phillip J. Eby wrote:
This is still rather rough, but I figured it's easier to let everybody fill in the remaining gaps by arguments than it is for me to pick a position I like and try to convince everybody else that it's right. :) Your feedback is requested and welcome.
I think you're actually highlighting a bigger issue with the behaviour of "yield" inside a "with" block, and working around it rather than fixing the fundamental problem. The issue with "yield" causing changes to leak to outer scopes isn't limited to coroutine style usage - it can happen with generator-iterators, too. What's missing is a general way of saying "suspend this context temporarily, and resume it when done". An example use-case not involving 'yield' at all is the "asynchronise" functionality. A generator-iterator that works in a high precision decimal.Context(), but wants to return values from inside a loop using normal precision is another example not involving coroutines. The basic idea would be to provide syntax that allows a with statement to be "suspended", along the lines of: with EXPR as VAR: for VAR2 in EXPR2: without: BLOCK To mean: abc = (EXPR).__with__() exc = (None, None, None) VAR = abc.__enter__() try: for VAR2 in EXPR2: try: abc.__suspend__() try: BLOCK finally: abc.__resume__() except: exc = sys.exc_info() raise finally: abc.__exit__(*exc) To keep things simple, just as 'break' and 'continue' work only on the innermost loop, 'without' would only apply to the innermost 'with' statement. Locks, for example, could support this via: class Lock(object): def __with__(self): return self def __enter__(self): self.acquire() return self def __resume__(self): self.acquire() def __suspend__(self): self.release() def __exit__(self): self.release() (Note that there's a potential problem if the call to acquire() in __resume__ fails, but that's no different than if this same dance is done manually). Cheers, Nick. P.S. Here's a different generator wrapper that could be used to create a generator-based "suspendable context" that can be invoked multiple times through use of the "without" keyword. If applied to the PEP 343 decimal.Context() __with__ method example, it would automatically restore the original context for the duration of the "without" block: class SuspendableGeneratorContext(object): def __init__(self, func, args, kwds): self.gen = None self.func = func self.args = args self.kwds = kwds def __with__(self): return self def __enter__(self): if self.gen is not None: raise RuntimeError("context already in use") gen = self.func(*args, **kwds) try: result = gen.next() except StopIteration: raise RuntimeError("generator didn't yield") self.gen = gen return result def __resume__(self): if self.gen is None: raise RuntimeError("context not suspended") gen = self.func(*args, **kwds) try: gen.next() except StopIteration: raise RuntimeError("generator didn't yield") self.gen = gen def __suspend__(self): try: self.gen.next() except StopIteration: return else: raise RuntimeError("generator didn't stop") def __exit__(self, type, value, traceback): gen = self.gen self.gen = None if type is None: try: gen.next() except StopIteration: return else: raise RuntimeError("generator didn't stop") else: try: gen.throw(type, value, traceback) except (type, StopIteration): return else: raise RuntimeError("generator caught exception") def suspendable_context(func): def helper(*args, **kwds): return SuspendableGeneratorContext(func, args, kwds) return helper -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia --------------------------------------------------------------- http://boredomandlaziness.blogspot.com

Nick Coghlan wrote:
P.S. Here's a different generator wrapper that could be used to create a generator-based "suspendable context" that can be invoked multiple times through use of the "without" keyword. If applied to the PEP 343 decimal.Context() __with__ method example, it would automatically restore the original context for the duration of the "without" block.
I realised this isn't actually true for the version I posted, and the __with__ method example in the PEP - changes made to the decimal context in the "without" block would be visible after the "with" block. Consider the following: def iter_sin(iterable): # Point A with decimal.getcontext() as ctx: ctx.prec += 10 for r in iterable: y = sin(r) # Very high precision during calculation without: yield +y # Interim results have normal precision # Point B What I posted would essentially work for this example, but there isn't a guarantee that the context at Point A is the same as the context at Point B - the reason is that the thread-local context may be changed within the without block (i.e., external to this iterator), and that changed context would get saved when the decimal.Context context manager was resumed. To fix that, the arguments to StopIteration in __suspend__ would need to be used as arguments when the generator is recreated in __resume__. That is, the context manager would look like: @suspendable def __with__(self, oldctx=None): # Accept argument in __resume__ newctx = self.copy() if oldctx is None: oldctx = decimal.getcontext() decimal.setcontext(newctx) try: yield newctx finally: decimal.setcontext(oldctx) raise StopIteration(oldctx) # Return result in __suspend__ (This might look cleaner if "return arg" in a generator was equivalent to "raise StopIteration(arg)" as previously discussed) And (including reversion to 'one-use-only' status) the wrapper class would look like: class SuspendableGeneratorContext(object): def __init__(self, func, args, kwds): self.gen = func(*args, **kwds) self.func = func self.args = None def __with__(self): return self def __enter__(self): try: return self.gen.next() except StopIteration: raise RuntimeError("generator didn't yield") def __suspend__(self): try: self.gen.next() except StopIteration, ex: # Use the return value as the arguments for resumption self.args = ex.args return else: raise RuntimeError("generator didn't stop") def __resume__(self): if self.args is None: raise RuntimeError("context not suspended") self.gen = self.func(*args) try: self.gen.next() except StopIteration: raise RuntimeError("generator didn't yield") def __exit__(self, type, value, traceback): if type is None: try: self.gen.next() except StopIteration: return else: raise RuntimeError("generator didn't stop") else: try: self.gen.throw(type, value, traceback) except (type, StopIteration): return else: raise RuntimeError("generator caught exception") def suspendable_context(func): def helper(*args, **kwds): return SuspendableGeneratorContext(func, args, kwds) return helper Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia --------------------------------------------------------------- http://boredomandlaziness.blogspot.com

Whoa, folks! Can I ask the gentlemen to curb their enthusiasm? PEP 343 is still (back) on the drawing table, PEP 342 has barely been implemented (did it survive the AST-branch merge?), and already you are talking about adding more stuff. Please put on the brakes! If there's anything this discussion shows me, it's that implicit contexts are a dangerous concept, and should be treated with much skepticism. I would recommend that if you find yourself needing context data while programming an asynchronous application using generator trampolines simulating coroutines, you ought to refactor the app so that the context is explicitly passed along rather than grabbed implicitly. Zope doesn't *require* you to get the context from a thread-local, and I presume that SQLObject also has a way to explicitly use a specific connection (I'm assuming cursors and similar data structures have an explicit reference to the connection used to create them). Heck, even Decimal allows you to invoke every operation as a method on a decimal.Context object! I'd rather not tie implicit contexts to the with statement, conceptually. Most uses of the with-statement are purely local (e.g. "with open(fn) as f"), or don't apply to coroutines (e.g. "with my_lock"). I'd say that "with redirect_stdout(f)" also doesn't apply -- we already know it doesn't work in threaded applications, and that restriction is easily and logically extended to coroutines. If you're writing a trampoline for an app that needs to modify decimal contexts, the decimal module already provides the APIs for explicitly saving and restoring contexts. I know that somewhere in the proto-PEP Phillip argues that the context API needs to be made a part of the standard library so that his trampoline can efficiently swap implicit contexts required by arbitrary standard and third-party library code. My response to that is that library code (whether standard or third-party) should not depend on implicit context unless it assumes it can assume complete control over the application. (That rules out pretty much everything except Zope, which is fine with me. :-) Also, Nick wants the name 'context' for PEP-343 style context managers. I think it's overloading too much to use the same word for per-thread or per-coroutine context. -- --Guido van Rossum (home page: http://www.python.org/~guido/)

On 10/20/05, Guido van Rossum <guido@python.org> wrote:
Whoa, folks! Can I ask the gentlemen to curb their enthusiasm?
PEP 343 is still (back) on the drawing table, PEP 342 has barely been implemented (did it survive the AST-branch merge?), and already you are talking about adding more stuff. Please put on the brakes!
Yes. PEP 342 survived the merge of the AST branch. I wonder, though, if the Grammar for it can be simplified at all. I haven't read the PEP closely, but I found the changes a little hard to follow. That is, why was the grammar changed the way it was -- or how would you describe the intent of the changes? It was hard when doing the transformation in ast.c to be sure that the intent of the changes was honored. On the other hand, it seemed to have extensive tests and they all pass. Jeremy

At 10:40 PM 10/20/2005 +1000, Nick Coghlan wrote:
Phillip J. Eby wrote:
This is still rather rough, but I figured it's easier to let everybody fill in the remaining gaps by arguments than it is for me to pick a position I like and try to convince everybody else that it's right. :) Your feedback is requested and welcome.
I think you're actually highlighting a bigger issue with the behaviour of "yield" inside a "with" block, and working around it rather than fixing the fundamental problem.
The issue with "yield" causing changes to leak to outer scopes isn't limited to coroutine style usage - it can happen with generator-iterators, too.
What's missing is a general way of saying "suspend this context temporarily, and resume it when done".
Actually, it's fairly simple to write a generator decorator using context.swap() that saves and restores the current execution state around next()/send()/throw() calls, if you prefer it to be the generator's responsibility to maintain such context.

Phillip J. Eby wrote:
Actually, it's fairly simple to write a generator decorator using context.swap() that saves and restores the current execution state around next()/send()/throw() calls, if you prefer it to be the generator's responsibility to maintain such context.
Yeah, I also realised there's a fairly obvious solution to my decimal.Context "problem" too: def iter_sin(iterable): orig_ctx = decimal.getcontext() with orig_ctx as ctx: ctx.prec += 10 for r in iterable: y = sin(r) # Very high precision during calculation with orig_ctx: yield +y # Interim results have normal precision # We get "ctx" back here # We get "orig_ctx" back here That is, if you want to be able to restore the original context just *save* the damn thing. . . Ah well, chalk up the __suspend__/__resume__ idea up as another case of me getting overly enthusiastic about a complex idea without looking for simpler solutions first. It's not like it would be the first time ;) Cheers, Nick. -- Nick Coghlan | ncoghlan@gmail.com | Brisbane, Australia --------------------------------------------------------------- http://boredomandlaziness.blogspot.com
participants (5)
-
Guido van Rossum
-
Jeremy Hylton
-
Josiah Carlson
-
Nick Coghlan
-
Phillip J. Eby