问题

在使用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等待主线程调用,但是主线程此时被阻塞所以造成了死锁。