问题
在使用Yooasset加载UI时,提示WebGL不允许同步加载,于是加载代码如下,一个同步一个异步
// 编辑器模式同步加载方式
public static T LoadView<T>() where T : ViewBase
{
T view = GameObject.FindObjectOfType<T>();
if (view == null)
{
string path = ResourceConfig.ViewPath + typeof(T).Name + ".prefab";
view = UnityEditor.AssetDatabase.LoadAssetAtPath<T>(path);
view = GameObject.Instantiate(view);
}
return view;
}
// 运行模式异步加载
public static async Task LoadViewAsync<T>() where T : ViewBase
{
T view = GameObject.FindObjectOfType<T>();
if (view == null)
{
var ab = YooAssets.GetPackage(ResourceConfig.ViewABName);
var handle = ab.LoadAssetAsync(ResourceConfig.ViewPath + typeof(T).Name);
await handle.Task;
_viewHandleDic[typeof(T).Name] = handle;
view = handle.AssetObject as T;
view = GameObject.Instantiate(view);
UIViewManager.AddToCacheViews<T>(view);
}
}调用处:
public static T GetView<T>() where T : ViewBase
{
ViewBase view = null;
if (!cacheViews.TryGetValue(typeof(T).Name, out view))
{
// #if UNITY_EDITOR
// view = ViewResourceManager.LoadView<T>();
// cacheViews.Add(typeof(T).Name,view);
// #else
// ViewResourceManager.LoadViewAsync<T>().Wait();
// #endif
ViewResourceManager.LoadViewAsync<T>().Wait();
RefelectInvokeMethod<T>("OnInit");
}
return view as T;
}在运行的时候卡死了,最开始我并没有找到是哪里卡死了,在我的理解里面,实在调用处主线程等待加载异步完成。
原因分析
前情提要:浅谈Unity线程
Unity 对象(如 GameObject、Transform)的内部状态没有设计为线程安全的,多线程同时修改会导致数据损坏。所以许多API只能在主线程调用。
Task和async/await的区别
之前我就疑惑过,.net框架下的Task和thread的区别,去看了官方文档,Task其实线程池的高级封装,当使用Task.Wait()时,线程会阻塞等待
而真正开启一个线程的,不是async函数,而是await语句,async语句会让编译器生成状态机,await则会开启一个线程,使用await时,线程不会阻塞
所以分析上述代码:
public static T GetView<T>() where T : ViewBase
{
// ..
ViewResourceManager.LoadViewAsync<T>().Wait(); //主线程阻塞,开始等待加载
// ..
}
public static async Task LoadViewAsync<T>() where T : ViewBase
{
// ...
await handle.Task; // 这里开始多线程,开始等待加载完毕
// ...
view = GameObject.Instantiate(view);
}
UnitySynchronizationContext
Unity专门的同步上下文,源码链接https://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Export/Scripting/UnitySynchronizationContext.cs
前文可知,Unity的很多API(比如Transform、GameObject)并不是线程安全的,所以在非主线程中调用时,Unity会将其上下文存储在 UnitySynchronizationContext 中。
这一部分参考Claude的回答:
实际运行示例
1. 跨线程调用Unity API的完整流程
public class ThreadingExample : MonoBehaviour
{
void Start()
{
Debug.Log($"主线程ID: {Thread.CurrentThread.ManagedThreadId}");
// 在其他线程执行工作
Task.Run(() =>
{
Debug.Log($"工作线程ID: {Thread.CurrentThread.ManagedThreadId}");
// 需要回到主线程执行Unity API
var context = SynchronizationContext.Current;
// 如果在主线程,context就是UnitySynchronizationContext
// 如果在其他线程,context可能是null
// 假设我们保存了主线程的context
mainThreadContext.Post((_) =>
{
Debug.Log($"回调线程ID: {Thread.CurrentThread.ManagedThreadId}");
// 这里安全调用Unity API
gameObject.SetActive(false);
}, null);
});
}
private SynchronizationContext mainThreadContext;
void Awake()
{
// 保存主线程的同步上下文
mainThreadContext = SynchronizationContext.Current;
}
}2. async/await如何使用SynchronizationContext
public async Task AsyncMethodExample()
{
Debug.Log($"开始: {Thread.CurrentThread.ManagedThreadId}");
Debug.Log($"SyncContext: {SynchronizationContext.Current?.GetType()}");
// 这个await相当于:
// 1. 检查SynchronizationContext.Current
// 2. 如果存在,注册continuation到这个context
// 3. 当Task完成时,通过context.Post执行continuation
await Task.Delay(1000);
Debug.Log($"继续: {Thread.CurrentThread.ManagedThreadId}");
Debug.Log($"SyncContext: {SynchronizationContext.Current?.GetType()}");
// 因为有UnitySynchronizationContext,这里回到了主线程
gameObject.SetActive(true); // 安全!
}
// 等价的手动实现
public Task ManualAsyncExample()
{
Debug.Log($"开始: {Thread.CurrentThread.ManagedThreadId}");
var context = SynchronizationContext.Current; // 获取当前上下文
return Task.Delay(1000).ContinueWith(task =>
{
if (context != null)
{
// 通过上下文执行后续代码
context.Post(_ =>
{
Debug.Log($"继续: {Thread.CurrentThread.ManagedThreadId}");
gameObject.SetActive(true);
}, null);
}
else
{
// 没有上下文,直接执行(可能不安全)
Debug.Log($"继续: {Thread.CurrentThread.ManagedThreadId}");
}
});
}(AI真好用啊uu们)
补充
Claude举的例子很清晰了,但是我依旧有一些疑问,await之后的语句是相当于content的continuation吗?这一部分代码总是会被放到SynchronizationContext由主线程执行吗,还是有主线程才能使用的API才是由主线程执行?
下面一些补充:
1.continuation是全捕获的,且统一由主线程执行,因为无法确定到底有没有UnityAPI,所以选择了最安全的全捕获
2.若在await之后没有UnityAPI,可以使用await Task.Run(() => { //不使用UnityAPI }).ConfigureAwait(false); 使用其他线程执行continuation
3.下面是Claude给的源码示例,可以看看
// 源代码
public async Task MyAsyncMethod()
{
Console.WriteLine("Before await");
await SomeAsyncOperation();
Console.WriteLine("After await"); // 这就是continuation
DoSomething(); // 这也是continuation的一部分
}
// 编译器生成的状态机(简化版)
public struct MyAsyncMethodStateMachine : IAsyncStateMachine
{
public int state;
public TaskAwaiter awaiter;
public void MoveNext()
{
switch (state)
{
case 0:
Console.WriteLine("Before await");
awaiter = SomeAsyncOperation().GetAwaiter();
if (!awaiter.IsCompleted)
{
state = 1;
// 注册continuation
awaiter.OnCompleted(MoveNext); // 关键:这里决定在哪个线程执行
return;
}
goto case 1;
case 1:
// 这就是continuation部分
Console.WriteLine("After await");
DoSomething();
break;
}
}
}结论
现在问题很清晰了,我们再次分析代码:
public static T GetView<T>() where T : ViewBase
{
// ..
ViewResourceManager.LoadViewAsync<T>().Wait(); //主线程阻塞,开始等待加载完毕
// ..
}
public static async Task LoadViewAsync<T>() where T : ViewBase
{
// ...
await handle.Task; // 这里开始多线程,开始等待加载完毕
// ...
view = GameObject.Instantiate(view); //这里是continuation,需要主线程执行,
//但是此时主线程已经被阻塞,循环等待,发生死锁!!
}
LoadVIewAsync方法中的view = GameObject.Instantiate(view);这一行,使用了主线程才能够使用的API,此时异步线程会将其作为continuation存入UnitySynchronizationContext等待主线程调用,但是主线程此时被阻塞所以造成了死锁。
参与讨论
(Participate in the discussion)
参与讨论