使用Task.Run调用同步方法时出现意外结果

我正在开发一个应用程序,在某个时候我正在对对象的运动进行一些CPU密集型计算。在某些时候,我正在使用Task.Run()来调用执行工作的函数,这会导致一些意外的结果,我不确定这是否会被视为竞争条件或是否具有其他名称。我的实际代码相当庞大,因此我将问题简化为可以在.net Framework控制台应用程序中运行的最小示例。

对于MWE,请考虑以下类,它具有3个字段和一个初始化它们的构造函数。它还具有一个report()子项,以便于调试。

Public Class DebugClass
    Public Variable1 As Double
    Public Variable2 As Double
    Public Variable3 As Double

    Public Sub New(Variable1 As Double,Variable2 As Double,Variable3 As Double)
        Me.Variable1 = Variable1
        Me.Variable2 = Variable2
        Me.Variable3 = Variable3
    End Sub

    Public Sub Report()
        Console.WriteLine()
        Console.WriteLine("Variable1: {0},Variable2: {1},Variable3: {2}",Variable1,Variable2,Variable3)
    End Sub
End Class

我还有另一个帮助程序功能,它以0到1秒的随机延迟代替了我的实际应用程序需要的CPU密集型工作:

Public Async Function RandomDelayAsync() As Task
    Await Task.Delay(TimeSpan.FromSeconds(Rnd()))
End Function

出于演示目的,我有2个版本的“工作”功能; Async的非异步版本。这些函数中的每一个均以DebugClass的实例作为参数,假装对其进行一些工作,然后简单地返回它作为输入得到的同一对象。 :

'Async version
Public Async Function DoSomeWorkAsync(WorkObject As DebugClass) As Task(Of DebugClass)
    Await RandomDelayAsync()
    Return WorkObject
End Function

'Synchronous version
Public Function DoSomeWork(WorkObject As DebugClass) As DebugClass
    RandomDelayAsync.Wait()
    Return WorkObject
End Function

最后,我有一个WaiterLoop,此函数Awaits创建了一个要完成的任务,将返回的对象的字段打印到控制台并将其从任务列表中删除。然后,它等待下一个完成。在我的实际应用程序中,从各个任务获得结果后,我将在此处进行更多计算,以查看哪些参数给出了最佳结果。

Public Async Function WaiterLoop(Tasklist As List(Of Task(Of DebugClass))) As Task
    Dim Completed As Task(Of DebugClass)
    Do Until Tasklist.Count = 0
        Completed = Await Task.WhenAny(Tasklist)
        Completed.Result.Report()
        Tasklist.Remove(Completed)
    Loop
End Function

现在,首先考虑我的Main()函数的这个版本:

Sub Main()
    Randomize()

    Dim Tasklist As New List(Of Task(Of DebugClass))

    Dim anInstance As DebugClass
    For var1 As Double = 0 To 5 Step 0.5
        For var2 As Double = 1 To 10 Step 1
            For Var3 As Double = -5 To 0 Step 1
                anInstance = New DebugClass(var1,var2,Var3)
                'adding an Async task to the tasklist
                Tasklist.Add(DoSomeWorkAsync(anInstance))
            Next
        Next
    Next

    WaiterLoop(Tasklist).Wait()
    Console.ReadLine()

End Sub

这里的输出完全符合我的期望,所有任务均已完成,并且对于每个参数组合,将一行打印到控制台。到目前为止一切都很好,当此行出现时,我所面临的问题再次出现:

Tasklist.Add(DoSomeWorkAsync(anInstance))

已替换为此行

Tasklist.Add(Task.Run(Function() DoSomeWork(anInstance)))

在这个新版本中,我不调用工作功能的Async版本,而是使用Task.Run在工作线程上运行正常的同步功能。这是发狂的地方。 突然,输出不再符合预期;

'This is the type of output i now get:
Variable1: 1.5,Variable2: 7,Variable3: -1

Variable1: 5,Variable2: 10,Variable3: 0

Variable1: 5,Variable3: 0

我现在创建的所有任务似乎都在引用DebugClass的相同实例,因为每次任务完成时,都会输出相同的输出。我不明白为什么会这样,因为我每次启动新任务之前先创建一个DebugClass的新实例:anInstance = New DebugClass(var1,Var3),然后是Tasklist.Add(Task.Run(Function() DoSomeWork(anInstance)))。一旦我将DebugClass的新实例分配给AnInstance,它就会“忘记”它存储的先前实例,对吗?每个创建的任务所引用的实例都应该独立于其他任务所引用的实例吗?

很显然,我误会了,但是如果有人可以向我解释这里发生了什么,我将不胜感激。

我也想知道为什么一个比另一个要快,但是除非与这个问题有关,否则我将把它保存在一个单独的问题中。

感谢您抽出宝贵的时间阅读本文章。

qq842663987 回答:使用Task.Run调用同步方法时出现意外结果

在引用变量时,Lambdas(即Function() DoSomeWork(anInstance))“关闭” *,而不是变量的当前值。

因此Function() DoSomeWork(anInstance) 表示“当您开始运行时,请对DoSomeWork current 值执行anInstance方法”。

您只有一个anInstance实例,因为您在循环外声明了它。

快速解决方案:在内部循环内移动Dim anInstance As DebugClass语句,这为您提供了每个循环一个变量实例,这就是您想要的。

另请参见Captured variable in a loop in C#,这在c#中基本上是相同的问题,并且在注释中有一些有用的讨论/链接

*关闭是一个大话题,我建议阅读https://en.wikipedia.org/wiki/Closure_(computer_programming)。很高兴在评论中进一步讨论。

本文链接:https://www.f2er.com/3138845.html

大家都在问