Introduction to C++20 Coroutines - Part 3 Some tips about life cycle when using coroutines
This series is related to C++20 coroutine.
part 1: Generator
part 2: co_await/co_return
part 3: Some tips about life cycle when using coroutines
part 4: Some examples about co_await
part 5: stackful/stackless and Symmetric Transfer
In the part 1 and the part 2 of the coroutine’s series, we have learned the basic usage of coroutines. In this post, I would like to provide some tips about life cycle to help avoid to get stuck in some traps.
The Life Cycle of The Return Type
1 |
|
In the above example, the program can run happily. However, there are some potential risks.
A Crash Happens
An issue is reproduced easily with little change as below:
1 |
|
It would crash! We would see Bus error
if compiling it with gcc
. Why?
Analysis
Let’s run it with gdb to observe the stack when it crashes.
1 |
|
We could find that the closest stack about the crash located when the coroutine was resumed. In this way, we have reason to suspect that the coroutine had been destroyed at that time.
Anyway, it’s definitely related to the normal construction of the Generator g
or Generator
‘s assignment.
The program will run without crash if we keep Generator g;
but comment out other code. Obviously, the issue source is from the latter of those two conjectures.
Note that we didn’t implement any move constructor and any move assignment in the above code. Therefore, at this statement g = tokens(str);
, g
was copied from a temporary object and the original object was deleted. At the same time, the coroutine was also destroyed! The crash would certainly happen since the coroutine never existed anymore.
Solution
Now, the reason is clear. Based on that, we can fix the crash.
1 |
|
The Life Cycle of The parameters
Now let’s look at another case with a very simple form:
1 |
|
It looks pretty fine, and we may have no solution to optimize it anymore.
Wild Resources
If we just change the form of parameters like this? The difference is the parameters of tokens
.
1 |
|
Actually, there is a risk that is not easy to spot - the std::string
was cosntructed from the raw string when tokens("Hello World")
was called but it was deleted as soon as the execution left the scope of tokens(...)
. It means after that it associated with a wild resource, and the wild resources we accessed was never valid!
Customize to Make it More Clearly
We can customize the parameters to reflect the life cycle more clearly.
1 |
|
Output:
1 |
|
My hypothesis gets verified!
Available Solutions
Therefore, we have 2 possible ways to solve this issue:
- Make sure the life cycle of any parameter covers the coroutine’s.
- Pass value instead of passing short-lived reference or pointer to make resources always valid, even though there is some extra overhead.
1 |
|
Output:
1 |
|
Note:
Remember to implement the move constructor of parameters. After all, those objects will be transferred to the coroutine, and will be transferred again but to promise_type
‘s constructor.
Manage Life Cycle Carefullly
Coroutine is powerful but it’s also dangerous. Remember to manage every component’s life cycle carefully and more carefully. Good luck. :)