I have a variable that tracks the progress of two coroutines.

For example:

var array = []

func add_1():
    await func_that_takes_10_seconds_to_complete()
    array.append(1)

func add_2():
    await func_that_takes_10_seconds_to_complete()
    array.append(2)

add_1()
add_2()

I know that if I called these functions from two different Threads, then I would need to protect my array with a Mutex.

Do I also need a Mutex in this situation, where two coroutines are running at the same time? Or is there a guarantee that they won't run in parallel and mess up my array?

  • xyz replied to this.
    • Edited

    brianrodri In Godot all of your code runs in a single main thread, unless you explicitly put some code into a separate thread. In which case you should always do mutex lock when accessing shared data, as per usual.

    xyz Ah, in Godot the word is actually a coroutine

    I'll try to update the question.

    • xyz replied to this.
      brianrodri changed the title to Do I need a mutex for coroutines running at the same time? .
      • Edited

      brianrodri Generally speaking, coroutines are not parallel by definition.
      In Godot they as well run in the same thread as everything else. They are just like regular functions with additional ability to exit and re-enter at arbitrary points in their code.
      So to directly answer your updated question - coroutines cannot cause classic race conditions. You can still have bugs caused by convoluted switching, of course. But there's no point in using mutexes with them. You need to worry about that only if you launch actual threads via Thread objects.

        xyz Awesome, I think that answers my question then thanks! Do you also know when in the game loop they're "re-entered"? It'd be helpful to know if it's always before _process, for example. If the answer is "it's something that Godot might change later", that'd be helpful to know too.

        Thanks again!

        • xyz replied to this.
          • Edited

          brianrodri A coroutine can only be awaken by a signal. If your code itself is emitting the signal, you know exactly when that is. Awaiting coroutines will resume and execute all of their code (or until the next await) "inside" your emit() call, i.e. - immediately.

          Likewise, for built in signals that are emitted by the engine, awaiting coroutines will resume during engine's internal emit() call. The exact point inside frame processing at which the engine emits a particular signal varies depending on the type of signal. So check the reference for each signal, it'll likely say when exactly it is emitted if that may be important for the signal recipient.

            xyz Oh that's even more convenient than I thought, great!

            There's one last edge case I'm curious about: how does await work on a coroutine that doesn't await anything?

            For example:

            func _process_thing():
                if _dont_process:
                    return
                else:
                    await thing
            
            func do_thing():
                _dont_process = true
                await _process_thing()
                finish_thing()
            
            do_thing()

            The docs say that control is immediately returned to the caller as soon as await is reached. But if the coroutine I'm calling doesn't wait for anything to resume, then when does it get its opportunity to run?

            • xyz replied to this.
              • Edited

              brianrodri If you mean like this:
              Coroutine that doesn't await is just a regular function. So awaiting for it will behave just like a regular call. If I understood your question correctly.

              func coroutine():
              	return
              	await something
              	print("DONE")
              
              await coroutine()

              Is equivalent to:

              func coroutine():
              	return
              	
              coroutine()

              EDIT: So your example would be equivalent to:

              func _process_thing():
                  return
              
              func do_thing():
                  _process_thing()
                  finish_thing()
              
              do_thing()
              • Edited

              brianrodri The docs say that control is immediately returned to the caller as soon as await is reached

              Only if you're awaiting for another coroutine (or a signal). Otherwise it will behave like a regular call and the execution will continue past the await statement.

              Perfect, I have a good understanding now. Thanks so much for your help!!

              • xyz replied to this.