I don't think co_unhandled_exceptions adds anything does it? Either we automate storing the thread_local on yield and loading on resume or we don't. If we do, then the current thread_local query works fine.
Good point, that's no more expensive than updating a linked list, and is more direct.
If we had a way to *write* to unhandled exceptions we could do this in coroutine library code and make it QOI for a given coroutine implementation, and that doesn't have to be coroutine specific: std::set_uncaught_exceptions(cnt). We can already inject arbitrary state into the coroutine frame that's hidden from the coroutine author: in folly we maintain a stack trace for the debugger to walk, as one example.
That would allow omitting the overhead in the case of coroutines that aren't expected to be long lived. Fairly ugly, though, to expect coroutine libraries to modify state like this.
All that said, uncaught_exceptions is a nasty piece of global state in the way of the optimizer, and this is a good example of more overhead that depending on it could create. It might be better to warn about using scope_fail across a yield point in the same way we need to be better at warning against use of locks or thread_locals in general. One big advantage that co_await/co_yield have over fibers is that we know where the yield points are and can make this logic work.
Yes, but the sort of actions in a coroutine that you're worried about throwing are usually suspend points.
This all feels unnecessary when there's an obvious way to make scope guards work across yield points, at the expense of composability, sure, but with no problem for how they'd be intended to be used.
On 4/13/23 05:16, Edward Catmur wrote:
> On Wed, 12 Apr 2023 at 15:27, Andrey Semashev via Std-Discussion
> <mailto:firstname.lastname@example.org>> wrote:
> On 4/12/23 18:53, Edward Catmur wrote:
> > On Mon, 10 Apr 2023, 08:57 Andrey Semashev via Std-Discussion,
> > <email@example.com
> > <mailto:firstname.lastname@example.org
> <mailto:email@example.com>>> wrote:
> > I don't think that updating a pointer during a context switch
> would be a
> > deal breaker. The co_unhandled_exceptions() mechanism only
> needs to know
> > the current coroutine state of the current thread, it doesn't
> need a
> > list. If a linked list is needed to be able to return from one
> > to another then such a list must be already in place.
> > Now that I think of it, co_unhandled_exceptions() might not
> require any
> > new pointers. The coroutine already has to keep a pointer to
> its state
> > somewhere to be able to reference its local variables. I don't
> see why
> > co_unhandled_exceptions() couldn't use that pointer.
> > But how can co_unhandled_exceptions() *find* that pointer? The current
> > coroutine state is not stored in per thread memory; it's solely on the
> > stack. (The linked list that's "already in place" is the stack
> The compiler could pass this pointer as a hidden parameter to
> co_unhandled_exceptions() based on implementation-specific attribute, or
> co_unhandled_exceptions() could be a compiler intrinsic to begin with.
> Or, if the pointer is stored in a known fixed location of the parent
> stack frame, co_unhandled_exceptions() could extract it from there. All
> this is to say this task doesn't seem impossible.
> > That said, you might be able to find it when unwinding through a
> > coroutine stack frame. I'm still thinking about it, but it seems
> like it
> > should work. In that case you've only got overhead added to coroutine
> > creation, coroutine footprint, unwinding through a coroutine, and
> > doubled the stack footprint and memory access of the success/failure
> > scope guards, compared to a C++17 implementation using
> > unhandled_exceptions only.
> Not sure what you mean by doubling the stack footprint and memory access
> of the scope guards. The size of the scope guards didn't change.
> But *how* do scope_success and scope_failure access the current
> coroutine's co_uncaught_exceptions() counter? Obviously it's not a
> problem if they're complete automatic objects of the coroutine frame,
> but otherwise? e.g. if they're subobjects or dynamically allocated? I'd
> think this would require stack walking (non-destructive unwinding),
> which is hugely expensive, or constructing a thread-local linked list of
> coroutines, which has continuous overhead.
I think you are confusing the storage used for the scope guard object
with the stack frame. co_uncaught_exceptions() doesn't need or use the
scope guard object, it doesn't care where it is allocated or whether it
exists at all. What co_uncaught_exceptions() *may* need is its caller's
stack frame, if it is implemented in such a way that it uses the stack
frame to obtain the pointer to the coroutine state (or to discover
whether it is a coroutine at all).
Now that I think of it more, perhaps using the caller's stack frame is
not a viable idea after all, since the immediate caller of
co_uncaught_exceptions() may not be a coroutine, but a normal function
called within a coroutine. However, as I noted earlier, there are other
possible implementations, including not involving TLS. But using TLS to
store a pointer to the current coroutine state would be the simplest
solution, of course.
Right. But that would require a linked list, since otherwise there's no way to restore the previous value when a coroutine suspends or returns.
> And what for scope_success and scope_failure that aren't constructed
> from within coroutines at all? How far do they look to determine to use
> uncaught_exceptions() instead?
The scope guard would always use co_uncaught_exceptions(). When called
not within a coroutine (meaning, there are no coroutines higher up the
stack), co_uncaught_exceptions() would be equivalent to the
uncaught_exceptions() we currently have.
And how does co_uncaught_exceptions() know that there are no coroutines higher up the stack? It sounds like a thread local linked list is a necessity.
> And what if a dynamically allocated scope_success/scope_failure is
> constructed in one coroutine, but then has ownership transferred to
> another stack (which may or may not be a coroutine)?
In that case, the cached number of uncaught exceptions may become not
actual, depending on what kind of transfer you make. Yes, this may break
scope_success/scope_fail, unfortunately. I'd be willing to mark such use
of scope_success/scope_fail UB, as this is arguably a case of the user
explicitly doing something incompatible with those scope guards' design,
as opposed to using scope guards in the conventional way within coroutines.
Right, so this design is still fairly fragile. Which behaviors would you define, beyond the absolute minimum of scope guards as complete automatic objects?
> > When I say "the destruction of the scope guard is caused by a
> > unwinding procedure" I mean literally that the stack unwinding
> > is present higher up in the call stack from the scope guard
> > I'm choosing this criteria because that's what makes most
> sense to me in
> > relation to the expected behavior of the scope guards.
> > That doesn't work. Having an unwind caused destructor above in the
> > from a scope guard destructor does not always mean that the scope
> > is in a failure state. It feels like we're going round in circles
> A scope guard destructor would conceal this information by setting the
> uncaught exceptions counter to zero, as described below.
> Is this a revision to your design, an extension, or an alternate design?
> I'm having trouble keeping track.
No, it's the same design. Or you could say it's an evolution of the same
design during the discussion. It's all about co_uncaught_exceptions()
and co_set_uncaught_exceptions(), the latter being used for concealing
the pending exceptions from the scope guard action, as I have shown earlier.
Is this a necessary part of the design or can it be omitted? Which scenarios does it help with?
Std-Discussion mailing list