Call parallel coroutines and wait for all of them to be over
Asked Answered
C

10

9

I have some coroutines:

IEnumerator a(){ /* code */ }
IEnumerator b(){ /* code */ }
IEnumerator c(){ /* code */ }

I want to create a coroutine that calls a, b and c in parallel but wait for all of them to finish before going on, something like:

IEnumerator d(){
    StartCoroutine(a());
    StartCoroutine(b());
    StartCoroutine(c());
    wait until all of them are over
    print("all over");
}

Obviously I could use a boolean for each coroutine to save its current state, but since this approach is not scalable, I'd prefer a more straight-forward solution.

Colley answered 15/12, 2019 at 22:13 Comment(8)
Does this answer your question? Wait for coroutine to finishAnaplasty
No. I know that yield StartCoroutine(a()) waits a coroutine to finish, but I want to start all of them at the same time, to execute in parallel. Each one has its own duration, and when the last of them end, d should print "all over".Colley
Coroutines are inherently single threaded. They can't run "in parallel" by their very nature. You need threads for thatAnaplasty
@Draco18s Not exactly. Coroutines are both single threaded and kind of (non-concurrently) parallel. They're basically enumerators (step-executed etc.) The real problem is: how to tell when Coroutine ended.Unforgettable
@AndrewŁukasik You're kind of right. But its wrong to think about them "executing in parallel." In any case, the only way to wait for a coroutine to finish is for the calling method to be a coroutine (or write your own flag system).Anaplasty
If I call (in a normal void function) a lot of StartCoroutine(a()), StartCoroutine(b()), etc., all of them will execute in parallel. Btw I'm thinking about having an int counting the number of ended coroutines, where all of them increase this int when over.Colley
@Colley No, they don't. Coroutines are a single-threaded managed scheduling system. It only looks like they execute in parallel because of how yield works. If you put a log line between each of your StartCoroutine lines and a log at the top of both a and b, you'll find that a runs first (all the way to the first yield), then b (all the way to the first yield). Feel free to attach a debugger, too.Anaplasty
At the level of abstraction Daniel is working with, they run in parallel. This feels like answering someone's question about using Newton's second law by telling them about relativity.Cornstarch
B
17

The method that I use, which is also a bit clear code and easy to use:

IEnumerator First() { yield return new WaitForSeconds(1f); }
IEnumerator Second() { yield return new WaitForSeconds(2f); }
IEnumerator Third() { yield return new WaitForSeconds(3f); }

IEnumerator d()
{
    Coroutine a = StartCoroutine(First());
    Coroutine b = StartCoroutine(Second());
    Coroutine c = StartCoroutine(Third());

    //wait until all of them are over
    yield return a;
    yield return b;
    yield return c;

    print("all over");
}
Bivalve answered 15/2, 2020 at 14:30 Comment(5)
@jrmgx The coroutines will run parallelly. "All over" will be printed after 3 seconds.Bivalve
The answer is correct and will run parallelly. Would run in sequence if it was like: return yield StartCoroutine(a()); @BiswadeepSarkar you forgot return statement in the coroutines and variable and function names are clashing. Maybe you wanna fix that to make it easier for people to copy/paste and test it out. Cheers.Pointdevice
@BiswadeepSarkar you are right indeed, note that the syntax is wrong for return yield new xxx and should be yield return xxx. Except that I'm happy to have learn something new today :)Chape
@Chape Glad, you liked my answer! I have updated the answer.Bivalve
Doesn't each yield statement wait for a frame? If you were iterating over a large array of coroutines wouldn't that cause it to freeze for several frames?Mange
C
5

Biswadeep Sarkar's answer is pretty good. I improved it a bit. Made a universal function for waiting for parallel coroutines. Feel free to use and further modify it.

  IEnumerator WaitForSomeCoroutines(params IEnumerator[] ienumerators)
    {
        Debug.Log($"Start time of parallel routines: {Time.time}");
        if (ienumerators != null & ienumerators.Length > 0)
        {
            Coroutine[] coroutines = new Coroutine[ienumerators.Length];
            for (int i = 0; i < ienumerators.Length; i++)
                coroutines[i] = StartCoroutine(ienumerators[i]);
            for (int i = 0; i < coroutines.Length; i++)
                yield return coroutines[i];
        }
        else 
            yield return null;
        Debug.Log($"End time of parallel routines: {Time.time}");
    }

So, imagine you have some routines:

 IEnumerator A(float time) { yield return new WaitForSeconds(time); }
 IEnumerator B(float time) { yield return new WaitForSeconds(time); }
 IEnumerator C(float time) { yield return new WaitForSeconds(time); }    
  

You want routine D waits for complete of routines A,B and C. You do:

  IEnumerator D()
        {
            // We wait until 3 other coroutines are finished.
           yield return StartCoroutine(WaitForSomeCoroutines(
                A(1f),
                B(3f),
                C(2f)));
    
           // Now we make our stuff.
           Debug.Log("Working on my stuff...");
        }

Result of using routine D

Condyle answered 22/9, 2021 at 12:58 Comment(0)
C
3

You can also use the underlying iterator behind the coroutine and call MoveNext yourself

In your example it will be something like

IEnumerator a(){ /* code */ }
IEnumerator b(){ /* code */ }
IEnumerator c(){ /* code */ }

IEnumerator d(){
    IEnumerator iea = a();
    IEnumerator ieb = b();
    IEnumerator iec = c();
    // Note the single | operator is intended here
    while (iea.MoveNext() | ieb.MoveNext() | iec.MoveNext()) {
        yield return null;
    }
    print("all over");
}

See the documentation about the | operator here https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/boolean-logical-operators#logical-or-operator-
It is basically an || operator but that will evaluate all your expressions thus effectively advancing each iterator even if another is already done.

Chape answered 1/10, 2020 at 7:30 Comment(0)
M
2

None of these answers satisfied me so I'm adding my own. We can create a custom yield instruction that allows us to wait until a condition is met. In this case, the condition will be that all coroutines have finished:

/* Can be used in MonoBehaviours as:
 * yield return new WaitForAll(this, <coroutine list>); */
public class WaitForAll : CustomYieldInstruction
{
    // Only wait until all coroutines have finished
    public override bool keepWaiting => done.NotDone();

    Done done;

    public WaitForAll(MonoBehaviour monoBehaviour, 
        params IEnumerator[] coroutines)
    {
        done = new Done(coroutines.Length);
    
        // Start all wrapped coroutines
        for (int i = 0; i < coroutines.Length; i++)
        {
            IEnumerator coroutine = coroutines[i];
            monoBehaviour.StartCoroutine(
                WaitForCoroutine(monoBehaviour, coroutine, done));
        }
    }

    // Keeps track of number of coroutines still running
    class Done
    {
        int n;

        public Done(int n)
        {
            this.n = n;
        }

        public void CoroutineDone()
        {
            n--;
        }

        public bool NotDone()
        {
            return n != 0;
        }
    }

    // Coroutine wrapper to track when a coroutine has finished
    IEnumerator WaitForCoroutine(MonoBehaviour monoBehaviour, 
        IEnumerator coroutine, Done done)
    {
        yield return monoBehaviour.StartCoroutine(coroutine);
        done.CoroutineDone();
    }
}

Then we can use it like this:

IEnumerator d() {
    yield return new WaitForAll(this, a(), b(), c());
    Debug.Log("All done");
}
Mange answered 18/8, 2022 at 17:8 Comment(1)
You really went 10 steps further on this.Colley
H
1

When you install this package (source) it can be realized as async-coroutine mix approach:

using System.Collections;
using System.Threading.Tasks;
using UnityEngine;

public class TestCoroutines : MonoBehaviour
{

    void Start () => D();

    IEnumerator A () { yield return new WaitForSeconds(1f); print($"A completed in {Time.time}s"); }
    IEnumerator B () { yield return new WaitForSeconds(2f); print($"B completed in {Time.time}s"); }
    IEnumerator C () { yield return new WaitForSeconds(3f); print($"C completed in {Time.time}s"); }

    async void D ()
    {
        Task a = Task.Run( async ()=> await A() );
        Task b = Task.Run( async ()=> await B() );
        Task c = Task.Run( async ()=> await C() );

        await Task.WhenAll( a , b , c );

        print($"D completed in {Time.time}s");
    }

}

Console output:

A completed in 1.006965s
B completed in 2.024616s
C completed in 3.003201s
D completed in 3.003201s
Hydrant answered 16/12, 2019 at 0:2 Comment(4)
Code above is all I wrote so no. But this package is required for that to work this way as it exposes proper extension methodsUnforgettable
This works for logging but I suspect that for any not thread safe Unity API this will fail and you would need to dispatch anything async back into the Unity main thread before accessing any Unity APIJournalese
@Journalese Task.Run starts a worker thread so yes - no gameObject API there, unfortunately.Unforgettable
If going async you should rather use UniTask which provides both a better async implementation (regarding allocations etc) and seamless async <-> Unity main thread integrationJournalese
B
1

Here is another feature/method for working with coroutines and IEnumerators, based on the answer by Biswadeep Sarkar.

IEnumerator CountTill(int numberToCountTill) {
    for (int i = 0; i < numberToCountTill; i++) {
        yield return i;
    }
    yield return "Done";
}
IEnumerator a;
IEnumerator b;
IEnumerator c;

IEnumerator d()
{
    a = CountTill(1);
    b = CountTill(3);
    c = CountTill(9);

    StartCoroutine(a);
    StartCoroutine(b);
    StartCoroutine(c);

    //wait until all of them are over
    while (a.Current.ToString() != "Done" && 
           b.Current.ToString() != "Done" && 
           c.Current.ToString() != "Done")
    {
        yield return "Waiting";
    }


    print("all over");
}

This is how you can check on the status of a coroutine (IEnumerator) by keeping a reference to the function, and checking up on the value periodically. Potentially you could also start doing something else mid execution of the other method, like update a progress bar to show what stage of loading you've reached, provided you change what you yield return appropriately. Though be careful of deadlocks if you have two methods trying to take turns with each other.

Bookish answered 6/12, 2021 at 6:20 Comment(0)
V
1

I liked FuzzyCat's solution because it integrates well with the rest of the Coroutines system and doesn't skip N frames like Biswadeep's, however the code was unnecessarily complicated and there were some obvious optimizations here and there, so I cleaned it up a bit.

using System.Collections;
using UnityEngine;

public class WaitForAll : CustomYieldInstruction {
    public override bool keepWaiting => remaining > 0;
    private int remaining;

    public WaitForAll (MonoBehaviour monoBehaviour, params IEnumerator[] coroutines) {
        remaining = coroutines.Length;
        foreach (IEnumerator coroutine in coroutines) {
            monoBehaviour.StartCoroutine(WaitForCoroutine(coroutine));
        }
    }

    IEnumerator WaitForCoroutine (IEnumerator coroutine) {
        yield return coroutine;
        remaining--;
    }
}
Vizcacha answered 7/4 at 14:56 Comment(0)
D
0

Biswadeep's answer is right but results in boilerplate if you need to do this all over or have dozens of coroutines to run. FuzzyCat's is nice but requires starting a duplicate coroutine just to decrement a counter. Oleksander's is the best, but it has bugs and needs to be cleaned up.

Here's the canonical version of Oleksander's answer, as an extension method:

public static class MonoBehaviorExtensions {
  public static IEnumerator WaitForAllCoroutines(this MonoBehaviour b, params IEnumerator[] yieldables) {
    var coroutines = new Coroutine[yieldables.Length];

    for (int i = 0; i < coroutines.Length; i++) {
      coroutines[i] = b.StartCoroutine(yieldables[i]);
    }

    foreach (var coroutine in coroutines) {
      yield return coroutine;
    }
  }
}
Delaminate answered 17/6, 2023 at 21:18 Comment(0)
A
0

You can use this in any combination

    public static IEnumerator RunParallel(IReadOnlyList<IEnumerator> routines)
    {
        bool moveNext;
        do
        {
            moveNext = false;
            
            foreach (var routine in routines)
                moveNext |= routine.MoveNext();

            yield return new WaitForEndOfFrame();
        } while (moveNext);
    }

    public static IEnumerator RunSequentially(IReadOnlyList<IEnumerator> routines)
    {
        foreach (var routine in routines)
        {
            while (routine.MoveNext())
                yield return new WaitForEndOfFrame();
        }
    }
Anceline answered 26/1 at 0:45 Comment(0)
H
-3

You can try Invoke("MethodName",timeinFloat) and add a counter(int)/a bool in each methods. When all of them are done running, based on the counter/bool condition, you can go ahead with execution.

If the Invoke time is set to 0, it runs in the next update frame cycle

Henebry answered 16/12, 2019 at 18:32 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.