zoukankan      html  css  js  c++  java
  • 侦探般的程序员之二 不等待的调用异步方法填坑小计

    概述

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

    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. 特定的用户:特定的用户数据缓存中没有,需要调用接口获取,而使用缓存的话 就没有调用接口的过程 实际异步方法也没有线程的切换
  • 相关阅读:
    POJ 2449 Remmarguts' Date(第k短路のA*算法)
    UESTC 1717 Journey(DFS+LCA)(Sichuan State Programming Contest 2012)
    HRBUST 1211 火车上的人数【数论解方程/模拟之枚举+递推】
    洛谷 P1372 又是毕业季I[数论/神坑规律题]
    洛谷 P1865 A % B Problem[筛素数/前缀和思想/区间质数个数]
    CCCC L2-003. 月饼[贪心/类似hdu贪心老鼠]
    二项式定理与杨辉三角
    Educational Codeforces Round 39 (Rated for Div. 2) B. Weird Subtraction Process[数论/欧几里得算法]
    洛谷 P1784 数独[DFS/回溯]
    Wannafly交流赛1 B 硬币[数学思维/贪心]
  • 原文地址:https://www.cnblogs.com/luoyeluoy/p/12961082.html
Copyright © 2011-2022 走看看