协程里的变量陷阱

先看脚本

 IEnumerator YeildMethod()
 {
     yield return new WaitForSeconds(2f);
     Debug.Log("2秒到了");
     while (true)
     {
         yield return new WaitUntil(() => Input.GetKeyDown(KeyCode.Space));
         Debug.Log($"Space: go={gameObject.name} goId={gameObject.GetInstanceID()} compId={GetInstanceID()} fps{fps}");
     }
 }
  private void Start()
  {
      startCount++;
      // 在第一次 Update 之前调用一次
      Debug.Log("<color=cyan>[3. Start]</color> - 逻辑开始");
      Debug.Log($"Start called {startCount} times on {GetInstanceID()}");
      StartCoroutine(YeildMethod());
  }
 private void Update()
 {
     fps++;
     if (showRepeatEvents) Debug.Log("[Update] - 每帧逻辑更新");
     if (NeedOnce)
     {
         Debug.Log($"Update 里的 fps {fps} Input.GetKeyDown(KeyCode.Space) = {Input.GetKeyDown(KeyCode.Space)}");
         NeedOnce = false;
     }
     if (Input.GetKeyDown(KeyCode.Space))
     {
         Debug.Log($"Update 里的 Space {fps}");
         NeedOnce = true;
     }
 }

初步理解是按一下 space 会在update里和协程里均产生一次打印(update触发后故意多一次打印不在内,只是为了做参考)
但是实际上
按下时
fps-> down:
update Input.GetKeyDown(KeyCode.Space) = true
run coroutine Input.GetKeyDown(KeyCode.Space) = true
fps -> down+1:
update Input.GetKeyDown(KeyCode.Space) = false
run coroutine Input.GetKeyDown(KeyCode.Space) = true
最后一个协程判定里 Input.GetKeyDown(KeyCode.Space) = true 引起了两次协程打印。这就很有意思了,
查到资源 说的是:

这也解释了两个现象(全部统一了)
① 为什么协程里会触发两次?

帧 909:EarlyUpdate → true → 第一个 WaitUntil 通过
帧 910:EarlyUpdate → 仍然 true → 第二个 WaitUntil 再通过

Update 中已经是 false,但已经晚了
② 为什么 Update 永远只触发一次?
因为 Unity 在 Update 阶段 严格保证 KeyDown 只在一帧内为 true
而且只在 Update 的“第一次消费”有效
✅ 官方 & 工程级结论(非常重要)
Input.GetKeyDown 只应该在 Update() 中使用,
绝对不要直接作为 WaitUntil 的 predicate。

以上chatgpt的解析,待彻底搞明白了再来补充,暂时记录下
突然想到了一个关键字 Volatile 有点点感觉相似之处 比如下面代码

namespace ConsoleApp1
{
    internal class Program
    {

        // 情况 A:不使用 volatile,会导致程序在 Release 模式下死循环
        private static bool _stop = false;

        // 情况 B:使用 volatile,程序会正常停止
        // private static volatile bool _stop = false;

        static void Main()
        {
            Console.WriteLine("线程正在启动...");

            Task.Run(() =>
            {
                int i = 0;
                // 在 Release 模式下,编译器可能将此处优化为 while(true)
                // 因为它认为在此线程内部没有修改 _stop 的操作
                while (!_stop)
                {
                    i++;
                }
                Console.WriteLine("子线程检测到 stop 改变,已跳出循环。");
            });

            Thread.Sleep(1000); // 等待 1 秒
            _stop = true;
            Console.WriteLine("主线程已将 _stop 设置为 true。");

            Console.ReadLine(); // 观察程序是否会打印出“已跳出循环”
        }
    }
}

然后release下运行之后

线程正在启动...
主线程已将 _stop 设置为 true。
posted @ 2026-01-05 15:32  stweily  阅读(2)  评论(0)    收藏  举报