Неожиданные результаты при использовании `Task.Run` для вызова синхронного метода - PullRequest
0 голосов
/ 08 ноября 2019

Я работаю над приложением, где в какой-то момент я делаю некоторые интенсивные вычисления процессора для кинематики объекта. В какой-то момент я использую 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 секунды:

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()function:

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 для запуска обычно синхронной функции в рабочем потоке. Это где s ** t попадает в вентилятор. Внезапно результат уже не такой, как ожидалось;

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

Variable1: 5,Variable2: 10,Variable3: 0

Variable1: 5,Variable2: 10,Variable3: 0

Variable1: 5,Variable2: 10,Variable3: 0

Variable1: 5,Variable2: 10,Variable3: 0

Variable1: 5,Variable2: 10,Variable3: 0

Каким-то образом все созданные мной задачи теперь относятся к одному и тому же экземпляру DebugClass, так как каждый раз, когда задача завершается, выводится один и тот же вывод. Я не понимаю, почему это происходит, потому что я создаю новый экземпляр DebugClass перед каждым новым запуском новой задачи: anInstance = New DebugClass(var1, var2, Var3), а затем Tasklist.Add(Task.Run(Function() DoSomeWork(anInstance))). Как только я назначаю новый экземпляр DebugClass на AnInstance, он «забывает» предыдущий экземпляр, который хранил, верно? И экземпляр, на который ссылается каждая из созданных задач, должен быть независимым от тех, на которые ссылаются другие задачи?

Очевидно, я ошибаюсь, но я был бы признателен, если бы кто-то мог объяснить мне, что происходитздесь.

Мне также интересно, почему один работает быстрее, чем другой, но я оставлю это для отдельного вопроса, если он не связан с этой проблемой.

Спасибо, что нашли время, чтобы прочитать это.

1 Ответ

3 голосов
/ 08 ноября 2019

Lambdas (то есть Function() DoSomeWork(anInstance)) 'close' * для ссылки на переменную NOT для ее текущего значения.

Таким образом, Function() DoSomeWork(anInstance) означает ', когда вы приступаете к выполнениюметод DoSomeWork для значения current anInstance '.

У вас есть только один экземпляр anInstance, потому что вы объявили его вне цикла.

Быстрое исправление: переместите оператор Dim anInstance As DebugClass во внутренний цикл, это даст вам один экземпляр переменной на цикл, что вам и нужно.

См. Также Захваченная переменная в цикле в C # , это в основном тот же вопрос в c # и имеет некоторые полезные обсуждения / ссылки в комментариях

* Закрытия - это большая тема, я бы рекомендовал прочитать https://en.wikipedia.org/wiki/Closure_(computer_programming). Рад обсудить далее в комментариях.

...