V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
edis0n0
V2EX  ›  .NET

接手屎山遇到了一个 for 循环内创建的 Task 取到的值总是超出 for 循环的结束条件的问题,研究了 6 个小时还没解决,请大佬帮忙看看

  •  
  •   edis0n0 · 2023-02-24 03:29:28 +08:00 · 4322 次点击
    这是一个创建于 639 天前的主题,其中的信息可能已经有所发展或是发生改变。

    故障代码:

    在开发环境中 DataSources.Count 最大为 2 ,也就是 dsIndex 只有可能是 0,1 ,在 Task.Factory 外下断点拿到的值也确实都是 0,1 ,但在 Task.Factory 里拿到的 dsIndex 却总是 2 ,已经超出了 for 循环结束条件了,难道这个 Task 在循环结束才被开始执行?应该要怎么修改比较好?

    22 条回复    2023-03-10 12:30:43 +08:00
    SMGdcAt4kPPQ
        1
    SMGdcAt4kPPQ  
       2023-02-24 03:33:33 +08:00 via Android
    猜一个外部修改闭包内部变量的问题
    SMGdcAt4kPPQ
        2
    SMGdcAt4kPPQ  
       2023-02-24 03:36:18 +08:00 via Android
    另外可以试试把代码和问题一起贴进 ChatGPT
    xiadong1994
        3
    xiadong1994  
       2023-02-24 03:38:10 +08:00
    试试吧 dsIndex 传进函数里而不要用外部的变量
    edis0n0
        4
    edis0n0  
    OP
       2023-02-24 03:39:07 +08:00
    补张断点变量内容的截图 https://i.ibb.co/cgLd9Sr/2.jpg
    @ComputerIdiot #1 操作 dsIndex 的就 for 循环一处呀
    SMGdcAt4kPPQ
        5
    SMGdcAt4kPPQ  
       2023-02-24 03:40:54 +08:00 via Android   ❤️ 1
    @edis0n0 就是 for 循环修改了 dsIndex ,解决方法是循环体里 dsIndex 赋值给新变量,原使用 dsIndex 的地方使用新变量
    edis0n0
        6
    edis0n0  
    OP
       2023-02-24 03:53:59 +08:00
    @ComputerIdiot #5 看起来确实是这问题,谢谢
    lsk569937453
        7
    lsk569937453  
       2023-02-24 07:10:52 +08:00   ❤️ 2
    你这个 Task.factory.startNew 是提交一个任务到线程池里执行。
    所以主线程的循环的 dsIndex=2 的时候,即你的 for 循环代码已经执行完的时候,你的异步任务才开始执行。
    Goooooos
        8
    Goooooos  
       2023-02-24 08:50:02 +08:00
    Java 里,内部类访问外部类的局部变量,需要 final 修饰,能避免出现这种 bug
    eraserking
        9
    eraserking  
       2023-02-24 08:50:57 +08:00   ❤️ 1
    应该把 dsIndex 作为参数传进 StartNew 的那个异步方法,它除了可以接收 Action 也可以 Action<object>的
    你这是捕获到的闭包
    500
        10
    500  
       2023-02-24 09:12:02 +08:00
    这应该是一个典型的 ”Linq to sql 延迟执行“ 问题
    wanei
        11
    wanei  
       2023-02-24 09:15:31 +08:00
    await
    ilingfeng
        12
    ilingfeng  
       2023-02-24 09:34:29 +08:00   ❤️ 1
    @lsk569937453 就是这个问题,Task.factory.startNew 只是代表提交一个 Task 任务到线程池,什么时候执行还得等 CPU 调度,不是立马执行
    maplefly
        13
    maplefly  
       2023-02-24 09:36:17 +08:00
    把那个 index 传入 task 里面就好了
    nothingistrue
        14
    nothingistrue  
       2023-02-24 10:04:03 +08:00   ❤️ 2
    我是做 java 的,语言不同,不过思想差不多。Task.Factory.StartNew(asycn ...),这是提交了个异步的任务,那它里面的执行时机就不确定了。你的猜测没错,这外面循环就 2 次,所以超大的概率,是循环结束后 Task 才开始执行。如果你的循环是 100 次的话,那么就有可能外面循环到 50 次的时候,第 30 次提交的 Task 开始执行。

    断点调试的时候,之所以你没看出来问题,是因为你打了断点把异步任务给强行变回了同步任务。你打了两个断点,for 循环那里给主任务打了断点,看变量值那一行给 Task 打了断点,这样 Task 的执行时机,被人为控制的跟主任务同步了。

    你的期望是,Task 里面的 dsIndex ,应该是 Task 提交时候的 dsIndex ,这样它就不能用主任务的 dsIndex 变量,因为后者是会被主任务更改值的。解决方法很简单,Task 里面不要直接使用 dsIndex 变量。final int dsIndexFinal = dsIndex ,Task 里面使用 dsIndexFinal 代替 dsIndex 即可。

    @Goooooos java final 并不能避免这种 bug 。你可以试试这段代码:
    final AtomicInteger a = new AtomicInteger(1);
    executorService.submit(()->{
    Thread.sleep(5000);
    System.out.println(a.get());
    });
    System.out.println(a.addAndGet(10));

    像楼主这样的问题确实能避免,不过那是因为像 int dsIndex 这样的基本类型变量,压根就不能直接传递给匿名内部类,然后 IDE 会提醒你把它赋值给一个新的 final Integer 或 final AtomicInteger 变量后再传给匿名内部类去使用。
    xcqc
        15
    xcqc  
       2023-02-24 11:06:08 +08:00
    for 或者 foreach 里面声明一个变量 j(名称自取),dsIndex 赋值给声明的变量 j ,用 dsIndex 的地方替换成 j
    zhy0216
        16
    zhy0216  
       2023-02-24 11:09:40 +08:00
    盲猜闭包的问题
    你再 for 循环内闭包外再声明一个变量
    闭包内用那个变量
    timethinker
        17
    timethinker  
       2023-02-24 12:20:05 +08:00
    不用猜,就是闭包引用外部变量的问题,index 的作用域对于所有的闭包都是同一个值,因此当异步执行的时候,闭包内获取的 index 大概率就是同一个值:2 。

    解决办法很简单,你把 Task.Factory.StartNew 这里的一块代码提取出来当作一个方法来执行,把 index 当作参数传入。
    nekochyan
        18
    nekochyan  
       2023-02-24 14:24:23 +08:00
    for (var i =0 ; i < 10; i++) {
    setTimeout(()=>{
    console.log(i);
    },0)
    }
    如果你会 js 的话会很明显的发现跟这段代码差不多的问题,输出的 i 全部是 10
    v2bsd
        19
    v2bsd  
       2023-02-24 18:05:52 +08:00
    glouhao
        20
    glouhao  
       2023-02-24 21:04:47 +08:00 via Android
    费眼睛的直接给 chatgpt 啊
    hez2010
        21
    hez2010  
       2023-03-10 12:28:13 +08:00 via Android
    很经典的错误
    dsIndex 在被捕获进 Task.Factory 里的 lambda 时被提升到堆了,并且生命周期被延长,Task.Factory 获得的实际上是对 dsIndex 的引用 (如果不这么做的话你就没法在闭包内修改外部的局部变量)
    所以当 Task.Factory 里面的代码被真正执行的时候,你外面的 for 循环已经把 dsIndex 加到 2 然后退出循环了,自然闭包里面拿到的值就会是 2
    hez2010
        22
    hez2010  
       2023-03-10 12:30:43 +08:00 via Android
    解决方法也很简单,自己加一个局部变量,不要直接用 for 的循环变量就行了.
    for (...) {
    var j = dsIndex;
    然后这里的 Task.Factory 里用 j 而不是 dsIndex
    }
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2200 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 21ms · UTC 01:34 · PVG 09:34 · LAX 17:34 · JFK 20:34
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.