Сразу скажу, красивого решения пока нет. Разработчики Unity сейчас работают над тем чтобы реализовать async/await
как надо, ждемс. Поэтому если не изобретать, то корутины вполне себе нормальное решение.
Смысл проблемы в том, что в Unity пока нет рабочего контекста синхронизации, то есть продолжение асинхронного метода будет выполнено на потоке из пула, так себя ведет SynchronizationContext
по умолчанию. Но при этом сам движок на уровне скриптов однопоточный. Поэтому нужна какая-то синхронизация.
Альтернативно вы можете запустить асинхронную операцию.
Task task = CloudCodeService.Instance.CallEndpointAsync("SetTime", arguments);
Куда-то этот таск положить, в поле например.
А затем например каждый кадр его проверять
if (task.IsCompleted)
{
try
{
// таск завершен
task.Wait(); // для Task
//var result = task.Result; // для Task<T>
// здесь можно что-то сделать с результатом или отреагировать на то что таск завершился.
}
catch (Exception ex)
{
Debug.Log(ex.ToString());
}
}
И всё, никаких корутин, только поллинг. Unity-стайл. :)
С другой стороны, async/await
не магия же какая-то. Можно и приручить, правда уже получится кода побольше. Из нехороших штук, придется использовать тот же поллинг, то есть Update
для разгребания очереди асинхронных коллбэков.
Давайте ради интереса и обучения создадим такой контекст синхронизации.
public class SingleThreadedSynchronizationContext : SynchronizationContext
{
private readonly ConcurrentQueue<(SendOrPostCallback, object)> _queue = new();
public override void Post(SendOrPostCallback d, object state)
{
_queue.Enqueue((d, state));
}
public override void Send(SendOrPostCallback d, object state)
{
if (Current == this)
d(state);
else
{
using ManualResetEventSlim mre = new();
ExceptionDispatchInfo edi = null;
Post(s =>
{
try
{
d(s);
}
catch (Exception ex)
{
edi = ExceptionDispatchInfo.Capture(ex); // ловим исключение в колбэке
}
mre.Set();
}, state);
mre.Wait();
edi?.Throw(); // выбрасываем пойманное исключение в вызывающей точке
}
}
public void Init(bool force = false)
{
if (Current is null || force)
SetSynchronizationContext(this);
else if (Current != this)
throw new InvalidOperationException($"Уже существует другой контекст синхронизации {Current.GetType().Name}, не удается проинициализировать контекст");
}
public bool ExecuteCallback()
{
if (_queue.TryDequeue(out var t))
{
(SendOrPostCallback d, object state) = t;
d(state);
return true;
}
return false;
}
}
Много всего непонятно, ну да ладно. Теперь как это вкрутить в Unity. Пусть будет вот такой скрипт.
public class SynchronizationBehavior : MonoBehavior
{
private SingleThreadedSynchronizationContext _context;
void Awake()
{
_context = new SingleThreadedSynchronizationContext();
_context.Init();
}
void Update()
{
while (_context.ExecuteCallback()) { } // разгрести очередь колбэков
}
}
Теперь этот скрипт в одном экземпляре нужно куда-то навесить.
Теперь вы сможете в любом другом скрипте выполнить асинхронный код не вывалившись из контекста синхронизации.
void Start()
{
WriteTimer("...");
}
public async void WriteTimer(string status)
{
try
{
var arguments = new Dictionary<string, object>
{
{ "name", ActiveModule.gameObject.name },
{ "status", status }
};
var response = await CloudCodeService.Instance.CallEndpointAsync("SetTime", arguments);
}
catch (Exception ex)
{
Debug.Log(ex.ToString());
}
}
В async void
методах обязательно оборачивайте весь код в try-catch
.
await Task.Run(()=> ...)
для асинхронных методов делать не нужно. Это бессмысленно.
Больше про однопоточный контекст синхронизации я писал здесь и здесь.