A post here inspired me to make an improved ParallelCoroutines runner!

I came across this post when I was about to ask how to run multiple awaits in parallel. It answered my question quite nicely, and I think kienhoang and jovialthunder wrote some good code, but I saw a way that ParallelCoroutines could be made more easily usable. So I’m posting the code for my own take on it here now. I also have it as a gist along with its license (MIT).

class_name ParallelCoroutines
extends RefCounted

## Emitted when all coroutines have completed from run_all()
signal completed

class QueuedCoroutine:
	var coroutine:Callable
	var callback:Callable
	func _init(p_coroutine, p_callback):
		coroutine = p_coroutine
		callback = p_callback

var _queued_coroutines:Array[QueuedCoroutine] = []
var _total_count:int = 0
var _completed_count:int = 0

## Each coroutine that completes will have its result added to results.
## await completed to ensure all results are present.
## results will contain null values for coroutines with no return value.
var results:Array = []

## Adds a coroutine to be started when run_all() is called.
## coroutine must be an asynchronous method i.e. it must call await at least once.
## callback can be provided if you wish to immediately act upon coroutine's completion.
## callback will be called with the result returned by coroutine.
## If coroutine does not return a value, the result will be null.
func append(coroutine:Callable, callback:Callable = func(_result):{}) -> ParallelCoroutines:
	#@warning_ignore does nothing for the warning thrown by the default lambda :(
	_queued_coroutines.append(QueuedCoroutine.new(coroutine, callback))
	return self

## Runs all coroutines added by append in parallel.
## returns completed Signal as a convenience.
## You may await completed or this method.
func run_all() -> Signal:
	_total_count = _queued_coroutines.size()
	var i = 0
	while i < _total_count:
		_run(_queued_coroutines[i])
		i += 1
	return completed
		
func _run(routine:QueuedCoroutine) -> void:
	var result = await routine.coroutine.call()
	results.append(result)
	routine.callback.call(result)
	_on_completed()

func _on_completed() -> void:
	_completed_count += 1
	if _completed_count >= _total_count:
		completed.emit()

Usage example:

extends Node2D

func _ready():
	call_routines()

func call_routines():
	var routines = ParallelCoroutines.new()
	routines.append(routine_0).append(routine_1, do_something_with_result).append(routine_2, do_something_with_result)
	await routines.run_all()
	print(routines.results)
	print(str(Time.get_unix_time_from_system()))
	
func routine_0():
	print("routine 0 started  " + str(Time.get_unix_time_from_system()))
	await get_tree().process_frame
	print("routine 0 complete " + str(Time.get_unix_time_from_system()))
	
func routine_1():
	print("routine 1 started  " + str(Time.get_unix_time_from_system()))
	await get_tree().create_timer(2.).timeout
	print("routine 1 complete " + str(Time.get_unix_time_from_system()))
	
func routine_2():
	print("routine 2 started  " + str(Time.get_unix_time_from_system()))
	await get_tree().create_timer(4.).timeout
	print("routine 2 complete " + str(Time.get_unix_time_from_system()))
	return "Not null!"
	
func do_something_with_result(result):
	if result != null:
		print("completed ", result)

If you try using it, please let me know if you have any questions or if you encounter any bug! There shouldn’t be any other than one annoying warning I can’t get rid of :sweat_smile:

If anyone knows how to provide a default Callable parameter without pissing off the compiler, I would be delighted if you’d share that with me.

3 Likes