landelare / ue5coro

A C++20 coroutine implementation for Unreal Engine 5 that feels almost native.
BSD 3-Clause Clear License
543 stars 48 forks source link

Waiting for a coroutine in a coroutine takes 1-Tick for the process to return #18

Closed aziogroup closed 9 months ago

aziogroup commented 9 months ago

Hello, I'm currently using UE5Coro to perform various tests. When I call a coroutine from within another coroutine and wait for it, I've noticed that there's a 1-Tick delay before the internal coroutine finishes and the external coroutine resumes processing. It turns out that I can avoid this delay by using co_await WhenAny(TCoroutine<>) instead of a simple co_await TCoroutine<> . Is this behavior intentional?

If it is intentional, is there a way to achieve the same behavior as WhenAny without using WhenAny when simply using co_await TCoroutine<>?

Thank you.

// -------- ATestActor. ------------- //
void ATestActor::BeginPlay()
{
    Super::BeginPlay();

    NestCoroutine(3, false);
    NestCoroutine(3, true);
}

UE5Coro::TCoroutine<> ATestActor::NestCoroutine(int DepthCount, bool bUseWhenAny, FForceLatentCoroutine)
{
    UE_LOG(LogTemp, Log, TEXT("[%llu] Depth = %d Coroutine Start"), GFrameCounter, DepthCount);

    if (DepthCount == 0)
    {
        co_await UE5Coro::Latent::NextTick();
        UE_LOG(LogTemp, Log, TEXT("[%llu] Depth = %d Coroutine End / bUseWhenAny = %d"), GFrameCounter, DepthCount, bUseWhenAny);
        co_return;
    }

    if (bUseWhenAny)
    {
        // Wait FAnyAwaiter
        co_await WhenAny(NestCoroutine(DepthCount - 1, bUseWhenAny));
    }
    else
    {
        // Wait TCoroutine<>
        co_await NestCoroutine(DepthCount - 1, bUseWhenAny);
    }

    UE_LOG(LogTemp, Log, TEXT("[%llu] Depth = %d Coroutine End / bUseWhenAny = %d"), GFrameCounter, DepthCount, bUseWhenAny);

Result

LogTemp: [4071] Depth = 3 Coroutine Start
LogTemp: [4071] Depth = 2 Coroutine Start
LogTemp: [4071] Depth = 1 Coroutine Start
LogTemp: [4071] Depth = 0 Coroutine Start
LogTemp: [4071] Depth = 3 Coroutine Start
LogTemp: [4071] Depth = 2 Coroutine Start
LogTemp: [4071] Depth = 1 Coroutine Start
LogTemp: [4071] Depth = 0 Coroutine Start
LogTemp: [4072] Depth = 0 Coroutine End / bUseWhenAny = 0
LogTemp: [4072] Depth = 0 Coroutine End / bUseWhenAny = 1
LogTemp: [4072] Depth = 1 Coroutine End / bUseWhenAny = 1
LogTemp: [4072] Depth = 2 Coroutine End / bUseWhenAny = 1
LogTemp: [4072] Depth = 3 Coroutine End / bUseWhenAny = 1
LogTemp: [4073] Depth = 1 Coroutine End / bUseWhenAny = 0
LogTemp: [4074] Depth = 2 Coroutine End / bUseWhenAny = 0
LogTemp: [4075] Depth = 3 Coroutine End / bUseWhenAny = 0
aziogroup commented 9 months ago

Remarks: Since LatentActionManager is processed in the order of registration, the problem seems to be caused by the fact that the resumption process of the external coroutine is not called until the next frame after the internal coroutine has finished.

WhenAll, WhenAny, etc. were registered and processed after the internal coroutine, so they behaved as intended.

landelare commented 9 months ago

This behavior is by design and documented. Latent coroutines are fundamentally owned and controlled by the latent action manager and can only complete when it processes them on its own tick. This matches BP and most existing Latent UFUNCTIONs from the engine, which is the primary focus of this mode of execution.

In some situations that I encountered, I managed to skip the extra tick by returning an inner TCoroutine instead of co_awaiting it.

aziogroup commented 9 months ago

Thank you for your response, Sorry for the inconvenience,

In some situations that I encountered, I managed to skip the extra tick by returning an inner TCoroutine instead of co_awaiting it.

If you don't mind, I would appreciate it if you could tell me what exactly you did in response.

landelare commented 9 months ago

@aziogroup Instead of

TCoroutine<> Fn1();
TCoroutine<> Fn2()
{
    DoStuff();
    co_await Fn1();
}

it's

TCoroutine<> Fn1();
TCoroutine<> Fn2()
{
    DoStuff();
    return Fn1();
}
aziogroup commented 9 months ago

Thank you very much! It seemed difficult to apply it to my case, but in a simple case this seemed to be the way to get around it.