侦探般的程序员之二 不等待的调用异步方法填坑小计

概述

用户反馈了一个问题,操作之后的消息没有收到,但是问题比较特殊:

  1. 只有在特定的业务流程下才会出现
  2. 相同的流程只有特定的用户才会出现

问题分析:

  • 代码的逻辑是这样的,先执行保存功能 如果保存成功了 就发送消息通知
  • 问题的关键在于 发送消息的方法是异步的 并且没有使用await等待 直接调用,基本结构如下:
        //保存
        public bool Save () {
            bool result = true;
            var t= SendMsgAsync();
            return result;
        }

        //发送消息
        private async Task SendMsgAsync () {
            Console.WriteLine ("开始发送消息")
            long num = 0;
            while (true) {
                Console.WriteLine ("仅执行一次");
                 await Task.Delay (5000);
                Console.WriteLine ("不执行");
            }

        }
  • 通过日志分析 就是程序运行到某一点之后 就不再向下运行了 这个比较诡异 于是打上断点调试了一下
  • 调试时的现象就是 发送消息的时候 执行到一个异步调用接口的方法时就没有进入下一个断点了 就消失不见,而且再没有进入断点的同时接口就返回了保存的结果

解决

  • 看到这个问题 怀疑时异步调用方法并且没有await导致 于是再调用 SendMsgAsync 加上await 测试,消息发送成功,于是按这个方式更新一版,保证用户业务正常。
  • 但是到这里,问题并没有解决 发送消息的逻辑比较复杂,用时有时会比较久,直接await的话接口会比较慢
  • 请教一个同事,同事表示异步方法不能这样用不能不等待 我说那Task.Run()? 试了试 Task.Run()问题也解决了
  • 但是这样感觉还是不够简洁 重要的是问题的原因显然不是不能await 要找到问题的本质,联想到之前遇到过的一个同步调用async方法导致死锁的问题,非常相似,怀疑跟SynchronizationContext有关
  • SynchronizationContext 这个引发的问题的特点是:console程序不会有问题 .NetCore的WebApi也不会有问题(因为实质上也是控制台) 于是将代码简化后分别放入控制台项目和.netCore 项目运行 果然没问题
  • 确定了SynchronizationContext 导致的问题之后 就简单了 这个问题的本质就是运行到异步方法时使用了新的线程运行,运行结束后要回到原始线程的状态时 获取不到或冲突导致
  • 观察之前线上的错误日志对应的线程id 但是杂乱无章各种乱入 然后在本地手工调用System.Threading.Thread.CurrentThread.ManagedThreadId 并记录日志 发现因为记录日志的方法做了二次封装 线程id实际时记录日志的线程id 记录日志是异步的。。
  • 于是写了一个Mock方法来复现问题并记录日志 发现 所有的有输出的日志都是同一个线程id 而且都是在SendMsgAsync await之前的才有输出 而core的代码 则是await后是另外的线程id
  • 于是在 await SendMsgAsync()方法加入.ConfigureAwait (false); 成功 Mock的方法成功走完了 但是实际的方法没有生效,仍然没有推送消息
  • 想起之前看到的clr via中说到 当运行到await的时候会根据一定情况来选择 是用当前进程运行 或者是 使用另一个线程运行,当前线程返回运行await后的方法 未来避免SynchronizationContext导致死锁
  • clr via给出的方法是 在所有awaait使用ConfigureAwait(false)来声明不用恢复上下文环境(因为不知道实际会是在那里使用新的线程运行) 或者是使用Task.Run()来运行 Task.Run 是一个干净的上下文不存在切换的我呢提
  • 第一个方法显然可操作性太差 毕竟调用链太长 第二个方法的话就是之前用过的
  • 既然是因为是否使用其它线程来执行是因为不知道那里切换的话 那我们就在SendMsgAsync第一行手工来切换 使用await Task.Delay(1).ConfigureAwait (false);来强制切换一次 想起之前写js也用过setTimeout(function(){},1);来强制切换,异曲同工
  • 修改还代码如下:
  •       //保存
          public bool Save () {
              bool result = true;
              var t= SendMsgAsync();
              return result;
          }
    
          //发送消息
          private async Task SendMsgAsync () {
              await Task.Delay(1).ConfigureAwait (false);
              Console.WriteLine ("开始发送消息")
              long num = 0;
              while (true) {
                  Console.WriteLine ("循环执行");
                   await Task.Delay (5000);
                  Console.WriteLine ("循环执行");
              }
          }
    
    

线上问题分析:

  1. 特定的流程导致:只有特定的流程才需要使用接口(接口有数据缓存)获取数据,在其它情况下没有使用接口,程序没有发生线程的切换
  2. 特定的用户:特定的用户数据缓存中没有,需要调用接口获取,而使用缓存的话 就没有调用接口的过程 实际异步方法也没有线程的切换
原文地址:https://www.cnblogs.com/luoyeluoy/p/12961082.html