C++ Logo


Advanced search

Re: 回复: Delay the judgement for coroutine function after the instantiation of template entity.

From: Jason McKesson <jmckesson_at_[hidden]>
Date: Fri, 22 Jan 2021 12:12:34 -0500
On Fri, Jan 22, 2021 at 2:12 AM chuanqi.xcq <yedeng.yd_at_[hidden]> wrote:
> >> To Jason McKesson
> > It's difficult to respond to you when your post intermingles responses
> to different posts that are making different points. If you don't want
> to send multiple e-mails, then at least make it clear which parts of
> the text are responding to which people. Don't jumble them up
> together.
> What's the tool do you use to send email? I need to copy and add '>' symbol in the front by hande. It looks like the paragraphs you replies are generated by the tool.

Just Gmail. But I don't recall ever having used an email program that
didn't handle replies automatically. And I'm talking about from 1995.

> >>>> To Jason McKesson
> > > > Let's say that we have a function X which is, by its nature, an
> > > asynchronous coroutine function. This means that X has to schedule its
> > > resumption based on some external asynchronous process, like doing
> > > file/networking IO, etc. Doing this is *why* you made the function a
> > > coroutine; it is the nature of X that it waits on something
> > > asynchronously. And let's say that we have some function Y which gets
> > > called by X.
> >
> > The picture I see is that X is a coroutine and Y is a caller of X which need to do something only after X made his job. So Y should be a coroutine too, isn't it?
> > You got that backwards; Y "gets called by X". And no, the caller of X
> > doesn't need to be a coroutine *either*. At some point, every
> > coroutine has to be called by some function that is not a coroutine.
> There is really a gap. Let's say that A is coroutine and B is function which has something to done only after A made its job. So we need to co_await A in the function body of B. Then the function B becomes a coroutine. And there is function C which need to wait B to made its job done. And C would become a coroutine too. The chain of coroutines in our codes comes from such a story.

I understand how `co_await` coroutines are viral. But at some point,
someone calls a coroutine without having to *be* a coroutine. `main`
can't be a coroutine, after all.

> >> To Jason McKesson
> > And most of the code between these two points *does not care* if suspending happens or not.
> I agree with the statement literally. To make it clear, most of the codes between these two points *doesn't care* if suspending happens or not. But these codes care about whether the callee has made its job done.
> >> To Jason McKesson
> > All of this adds up to a textbook example of when to use stackful
> coroutines. They can suspend through *anything*; none of the code
> between the source and the receiver needs to know they are in a
> coroutine.
> I agree with this. In fact, we had made experiments to use stackful coroutine to refactor our codes.
> >> To Jason McKesson
> > So what we come down to is this: you want this feature so that you can
> (ab)use stackless coroutines in a scenario that is almost tailor-made
> for stackful coroutines. And stackful coroutines would almost
> certainly alleviate your performance problems in less asynchronous
> cases, since each function in the graph won't be its own heap
> allocation.
> But I can't agree with that we are abusing stackless coroutine. At least, we get very high performance gain and stability improvement when the concurrency is high by refactoring the codes into stackless coroutine. In fact, all of us think it is a successful experiment to refactor these codes use stackless coroutine.

And my point is that you would achieve equal-if-not-greater success
with stackful coroutines in this particular use case. And you wouldn't
need to rewrite half your codebase to do it.

> >> To Jason McKesson
> > So I would say that this is not a good motivating case for the change
> to the standard, since you're only encountering this problem because
> you're writing your code wrong.
> Same with above, we don't think it is wrong to use stackless coroutine to refactor our codes.
> >> To Jason McKesson
> > You misunderstood my point. In one instantiation, you had a function
> that returned an `int`; in its coroutine form, it returned a
> `task<int>`. It doesn't much matter if the coroutine form is a "true"
> coroutine or just something that returns a `task<int>`. What matters
> is that the way the caller *uses* the function must change.
> > Broadly speaking, if you have a template function, instantiating it
> with different parameters may change its return type, but it shouldn't
> unexpectedly change the basic way you *interact* with that kind of
> type. And I know there are functions in the standard library that
> violates those rules (`any_cast` being the most prominent). But it's
> not a thing we should encourage.
> To my understand, your point here is that we *shouldn't* change the return type by template parameters sicne it is a bad practice.

No, I specifically allowed for changes to the return type. My point
was about the *nature* of the return type. A function might return
some `optional<T>`, where `T` is decided based on template parameters
to that function. But the function should not sometimes return a `T`
and other times return `optional<T>`. Or sometimes an `optional<T>`
and other times a `T*`. Callers of such a function have to interact
with the return value in different ways arbitrarily.

Such functions should have different names whenever possible.

> But in fact there is two things, the static-if we want is a language feature and the example we give above is an application. And we can give an example that the return type of the template function wouldn't change. For example, both version of func is returning Task<int>:
> ```
> template<bool UseCoro>
> Task<int> funcA(...) {
> if constexpr (UseCoro)
> co_return co_await funcB<UseCoro>(...);
> else
> return Task<int>(funcB<UseCoro>(...).get()); // We can implement `get` by conditional variable or stackful coroutines.
> }
> ```
> And the caller of funcA would always get a Task<int>. Then the caller could use co_await or `get` to get the value. Although I don't know a specific work situation for this situation right now.
> All I want to say is the static-if is a language feature, and the user could use these feature to do their applications. Although you may say all these applications I give are bad practices and the feature shouldn't be enabled, I still think it is odd that constexpr-if wouldn't work for coroutine.

But if the only reason this is a problem is that you're writing poor
code, that's not a good reason to change the language to make it
easier to write poor code. This is *why* proposals need good
motivation: a proposed language change needs to be more than just "I
think the language would be better/more regular/etc". It needs to be
"here's a problem many people are having, and this language change is
the *best* way to solve it".

> >> To Jason McKesson
> > All of this adds up to a textbook example of when to use stackful
> coroutines. They can suspend through *anything*; none of the code
> between the source and the receiver needs to know they are in a
> coroutine.
> Reply to this paragraph again for something unrelated to previous discussion. From my work exeperience, the stackful coroutine is really easy to use and easy to understand, while the stackless coroutine stands in the opposite position exactly. Every time we want to make our c++ projects to use c++20 coroutine, we always need to refactor the codes for monthes to get some performance gains only in some cases. But as you said, stackful coroutine would perfom better when the concurrency isn't high. So the question I want to ask is, in what situation, we should use stackless coroutine instead of stackful coroutine? Or maybe we need to discuss this question in other place.

Which to use is a matter of the distance between the source caller and
the asynchronous operation.

The "source caller" is the function C which invokes an asynchronous
process (AP). C is the source caller because it knows that it is
invoking an asynchronous process and that C is not going to *wait* on
that process to finish at this point.

If you're doing UI programming, and the user enters some data you need
to shove into the database, C is the UI function that says "Shove this
into the DB, I'm going back into the UI routine, so let me know when
you're finished". AP is the DB function that does the shoving of the
data asynchronously.

The asynchronous operation (AO) is the actual thing (or things) that
does the waiting within the AP call graph. If the AP does some file
IO, AO is the function that actually does file IO, halting the
coroutine's execution until it is finished, and will schedule the
resumption of AP. That is, AP at some point invokes one or more AOs.

When should AP be a co_await coroutine vs. a stackful one? The
important thing to note is that this should be an *implementation
detail* of AP. It's a choice that AP makes that is nobody's business
but AP.

AP should be a stackless coroutine if the call graph between the
beginning of AP and all of its AOs is *short*. Ideally, the AP root
function should directly call the AOs, but there can be one or two
functions between them. The more functions there are between them, the
more places you have to add `co_await`, and the less broadly useful
those functions become (since anyone trying to use those functions
must treat them as coroutines).

AP also should not be a stackless coroutine if AP can sometimes be
invoked synchronously. If this is the case, then being a stackful
coroutine makes that much easier; the AOs can use some mechanism to
detect if they're being invoked synchronously and not invoke the
suspension mechanism or whatever. Co_await is a static property of a
function, whereas being a stackful coroutine can be a dynamic property
of how you invoke the function.

Received on 2021-01-22 11:12:47