[Python-ideas] concurrency-local namespaces

Nathaniel Smith njs at pobox.com
Sat Apr 25 03:03:35 CEST 2015


On Wed, Apr 22, 2015 at 11:51 AM, Yury Selivanov
<yselivanov.ml at gmail.com> wrote:
> Guido, Nathaniel,
>
> On 2015-04-22 2:45 PM, Guido van Rossum wrote:
>>
>> I'll have to leave this up to you and Nathaniel and others to ponder, I've
>> run out of time to think about more issues. Sorry! (I do think that the
>> conclusion of that patch was to go with current_task() intead.)
>
>
> Yes, that was the conclusion.  Because I was interested to add
> this functionality for web frameworks, where, as Nathaniel
> mentioned, you control the top-level Task object.  So I gave up
> on pushing the 'context' idea.
>
> If we want to add a generic thread-local-like object to
> coroutines, I think that it has to be done on an asyncio
> event loop level.  Because event loop is not just about
> coroutines, it's also about callbacks, and it is useful
> to *avoid* loosing context in them.
>
> Nathaniel, I'd be glad if you could glance over my old proposal.
> It's based on ideas I expressed in this thread:
> https://groups.google.com/forum/#!topic/python-tulip/zix5HQxtElg

Hi Yury,

Unfortunately I haven't had cause to write any async code in the last
~5 years or so, so my knowledge is all out of date and I'm not up on
the terminology there. With that in mind... I have two fairly basic
questions after reading your proposal:

1) Can everyone really agree on how you fetch the current "task
context" in an async framework? E.g., your proposal talks about
looking at attributes on the tulip main loop object. For a project
like numpy that doesn't involve any async stuff itself, but is
regularly used alongside multiple existing frameworks (tornado,
twisted, asyncio, etc.), is there some standard way to find "the main
loop" that works on all of them?

2) How fast can we make accessing "task-local storage"? The reason I
ask is, again, numpy, where we essentially have a single word-sized
bitmask (corresponding to the IEEE 754 control word) that we need to
stash somewhere and then access on every single arithmetic operation.
This means that the cost of accessing this thing is a non-trivial
speed bottleneck. Classic thread-local storage is great for this,
because it's heavily optimized by all common platforms, to the point
where accessing it is basically just following a pointer or two. Any
API that involves calling Python methods on every access is definitely
not going to fly here -- especially since this would be additional
overhead on *every* user of numpy, even the vast majority of users who
aren't writing any async code at all.

These questions make me wonder if it wouldn't be better to focus on
fixing 'with' instead, since it moves the problem closer to the actual
affected code -- in particular, a context manager doesn't need to know
anything about mainloops to manage state, and it doesn't have any
impact on people who aren't using

A straw man proposal would be something like:

-----

Currently, the code

        with EXPR as VAR:
            PARTIAL-BLOCK-1
            f((yield foo))
            PARTIAL-BLOCK-2

is equivalent to (cribbing from PEP 343):

        mgr = (EXPR)
        exit = type(mgr).__exit__  # Not calling it yet
        value = type(mgr).__enter__(mgr)
        exc = True
        try:
            try:
                VAR = value  # Only if "as VAR" is present
                PARTIAL-BLOCK-1
                f((yield foo))
                PARTIAL-BLOCK-2
            except:
                exc = False
                if not exit(mgr, *sys.exc_info()):
                    raise
        finally:
            if exc:
                exit(mgr, None, None, None)

Starting in 3.xx, two new methods __suspend__ and __resume__ are added
to the context manager protocol, and the above code instead becomes:

        mgr = (EXPR)
        exit = type(mgr).__exit__  # Not calling it yet
        #### <NEW STUFF>
        suspend = getattr(type(mgr), "__suspend__", lambda: None)
        resume = getattr(type(mgr), "__resume__", lambda: None)
        #### </NEW STUFF>
        value = type(mgr).__enter__(mgr)
        exc = True
        try:
            try:
                VAR = value  # Only if "as VAR" is present
                PARTIAL-BLOCK-1
                #### <NEW STUFF>
                suspend()
                tmp = yield foo
                resume()
                f(tmp)
                #### </NEW STUFF>
                PARTIAL-BLOCK-2
            except:
                exc = False
                if not exit(mgr, *sys.exc_info()):
                    raise
        finally:
            if exc:
                exit(mgr, None, None, None)

If a yield occurs inside multiple nested 'with' blocks, then they are
__suspend__'ed from the inside out, and __resume__'ed from the outside
in. 'yield from' can be reduced to 'yield' (as described in PEP 380),
and then the above transformation is applied.

Note that this is orthogonal to PEP 492, because the problem being
solved is caused by any 'with' block containing a yield statement. The
proposed 'async with' syntax handles a different issue, where
__enter__ and __exit__ may need to yield. If PEP 492 is accepted, then
analogous changes should also be made to the 'async with' syntax
(e.g., by adding __asuspend__ and __aresume__ to the async context
manager protocol).

-----

If there's interest I can throw the above into a quick PEP.
Unfortunately I don't have the bandwidth to implement it (recall that
I don't actually use asyncio :-)), nor the relevant knowledge of the
compiler -- though I *think* there shouldn't be any particularly nasty
problems in implementing it, since the compiler has static knowledge
of the presence of all yield points? In particular this means that
even the getattr() calls at the top can be optimized out for any
'with' blocks that don't actually contain any yield points, so the
proposal has zero impact on non-async code. For async code, the main
downside is that this could make yield's even more expensive than they
currently are -- but this is only when within a 'with' block, and I
suspect that this cost is small in any case.

-n

-- 
Nathaniel J. Smith -- http://vorpus.org


More information about the Python-ideas mailing list