Evo*_*lor 7 c# unity-game-engine async-await
注意:这个问题是 Unity 特有的。
我读到这async void是要避免的。 我尝试使用 来执行此操作Result,但我的应用程序一直锁定。如何避免使用 async void?
public async void PrintNumberWithAwait()
{
int number = await GetNumber();
Debug.Log(number); //Successfully prints "5"
}
public void PrintNumberWithResult()
{
int number = GetNumber().Result;
Debug.Log(number); //Application Freezes
}
private async Task<int> GetNumber()
{
await Task.Delay(1000);
return 5;
}
Run Code Online (Sandbox Code Playgroud)
我认为这是正确的,但我一定错过了一些东西。如何在没有 的情况下使用 async/await async void?
我使用以下代码单独运行测试(一次注释掉一个):
PrintNumberWithAwait();
PrintNumberWithResult();
Run Code Online (Sandbox Code Playgroud)
简洁版本
Unity 的同步上下文是单线程的。所以:
详细版本
你说你正在使用Unity。Unity“99%”是一个单线程框架。所以我想这段代码是在主 UI 线程上执行的。
让我们详细了解一下执行 PrintNumberWithResult() 时代码执行的操作。
解决方案 永远不要调用.Result。
如果您想触发并忘记任务操作,请使用 Task.Run(() => )。可是等等!这在 Unity 中不是一个好主意!(也许它在其他 .NET 应用程序中)。
如果您在 Unity 中使用 Task.Run(),您将强制使用默认的 .NET TaskScheduler 执行异步代码,该任务使用线程池,并且如果您调用某些与 Unity 相关的 API,则可能会导致一些同步问题。
在这种情况下,您想要做的是使用 async void (实际上并不是因为与异常处理相关的原因),一个您永远不会等待的异步任务方法(更好),或者可能使用一个库作为UniTask来使用 Unity 进行异步等待(我认为最好的)。
这个答案非常完整,并且很好地解释了“问题”/主题。
但稍微扩展一下,您的async void“作品”示例有一个原因:Debug.Log同步上下文并不重要。您可以安全地Debug.Log退出不同的线程和后台任务,Unity 会在控制台中处理它们。但是,一旦您尝试使用仅在主线程上允许的 Unity API 中的任何内容(基本上是立即依赖于或更改场景内容的所有内容),它可能会中断,因为不能保证最终会出现await在主线程上。
然而,现在怎么办?
没有什么真正反对在 Unity 中使用Threadand !Task.Run
您只需确保将所有结果分派回 Unity 主线程即可。
所以我只想举一些实际的例子来说明如何做到这一点。
经常使用的东西是所谓的“主线程调度程序”..基本上只是一个ConcurrentQueue允许您从任何线程回调,然后Enqueue在Unity 主线程上的例程中调用它们的东西。ActionTryDequeueUpdate
这看起来有点像例如
/// <summary>
/// A simple dispatcher behaviour for passing any multi-threading action into the next Update call in the Unity main thread
/// </summary>
public class MainThreadDispatcher : MonoBehaviour
{
/// <summary>
/// The currently existing instance of this class (singleton)
/// </summary>
private static MainThreadDispatcher _instance;
/// <summary>
/// A thread-safe queue (first-in, first-out) for dispatching actions from multiple threads into the Unity main thread
/// </summary>
private readonly ConcurrentQueue<Action> actions = new ConcurrentQueue<Action>();
/// <summary>
/// Public read-only property to access the instance
/// </summary>
public static MainThreadDispatcher Instance => _instance;
private void Awake ()
{
// Ensure singleton
if(_instance && _instance != this)
{
Destroy (gameObject);
return;
}
_instance = this;
// Keep when the scene changes
// sometimes you might not want that though
DontDestroyOnLoad (gameObject);
}
private void Update ()
{
// In the main thread work through all dispatched callbacks and invoke them
while(actions.TryDequeue(out var action))
{
action?.Invoke();
}
}
/// <summary>
/// Dispatch an action into the next <see cref="Update"/> call in the Unity main thread
/// </summary>
/// <param name="action">The action to execute in the next <see cref="Update"/> call</param>
public void DoInNextUpdate(Action action)
{
// Simply append the action thread-safe so it is scheduled for the next Update call
actions.Enqueue(action);
}
}
Run Code Online (Sandbox Code Playgroud)
当然,您需要将其附加到场景中的对象上,然后从任何您可以使用的地方,例如
public void DoSomethingAsync()
{
// Run SomethingWrapper async and pass in a callback what to do with the result
Task.Run(async () => await SomethingWrapper(result =>
{
// since the SomethingWrapper forwards this callback to the MainThreadDispatcher
// this will be executed on the Unity main thread in the next Update frame
new GameObject(result.ToString());
}));
}
private async Task<int> Something()
{
await Task.Delay(3000);
return 42;
}
private async Task SomethingWrapper (Action<int> handleResult)
{
// cleanly await the result
var number = await Something ();
// Then dispatch given callback into the main thread
MainThreadDispatcher.Instance.DoInNextUpdate(() =>
{
handleResult?.Invoke(number);
});
}
Run Code Online (Sandbox Code Playgroud)
如果您正在进行大量异步操作并希望在某个时刻将它们全部分派回主线程,那么这是有意义的。
另一种可能性是使用Coroutines。协程基本上有点像临时Update方法(默认情况MoveNext下每帧调用IEnumerator一次),因此您可以重复检查您的任务是否已在主线程上完成。这就是 Unity 自身的用途,例如UnityWebRequest,看起来有点像
public void DoSomethingAsync()
{
// For this this needs to be a MonoBehaviour of course
StartCorouine (SomethingRoutine ());
}
private IEnumerator SomethingRoutine()
{
// Start the task async but don't wait for the result here
var task = Task.Run(Something);
// Rather wait this way
// Basically each frame check if the task is still runnning
while(!task.IsCompleted)
{
// Use the default yield which basically "pauses" the routine here
// allows Unity to execute the rest of the frame
// and continues from here in the next frame
yield return null;
}
// basically same as
//yield return new WaitUntil(()=>task.IsCompleted);
// Safety check if the task actually finished or is canceled or faulty
if(task.IsCompletedSuccessfully)
{
// Since we already waited until the task is finished
// This time Result will not freeze the app
var number = task.Result;
new GameObject (number.ToString());
}
else
{
Debug.LogWarning("Task failed or canceled");
}
}
private async Task<int> Something ()
{
await Task.Delay(3000);
return 42;
}
Run Code Online (Sandbox Code Playgroud)