Ваше решение счетчика является действительным. Что бы вы ни делали, вам придется отслеживать все ваши запросы и понимать, когда они поступают (когда число достигает нуля).
Вы можете делать разные вещи для очистки своего кода, например помещать всю эту реализацию в некоторый класс MultiAsyncWaiter, который возвращает событие после завершения. Но фундаментальный смысл останется прежним: следите за ними, пока они все не вернутся.
Вы правы насчет небезопасности int. Если вы используете блокированные операции (см. Комментарии) или блокируете переменную, вы можете обеспечить безопасность своей реализации.
Почему ключевое слово volatile не работает: при изменении переменной несколькими потоками для декремента требуется взаимосвязанная операция, технически операция чтения + записи. Это связано с тем, что другой поток может изменить значение между чтением и записью.