在 BackgroundWorker 中使用 Await 会引发 WorkerComplete 事件

Using Await in BackgroundWorker causes WorkerComplete event to be raised

我有这个奇怪的问题。我需要从后台工作人员中调用一个进程

Private Shared _process As Process
Private Shared _StartInfo As ProcessStartInfo
Private WithEvents _bwConvertMedia As New BackgroundWorker

这是 DoWorkAsync 中的工作

Private Async Sub _bwConvertMedia_DoWorkAsync(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles _bwConvertMedia.DoWork
  For AI = 1 To 100
    _StartInfo = New ProcessStartInfo(".\mycmd.exe", "-1")
    _StartInfo.RedirectStandardOutput = True
    _StartInfo.UseShellExecute = False
    _StartInfo.CreateNoWindow = True
    _StartInfo.RedirectStandardError = True

    _process = New Process() With {.EnableRaisingEvents = True, .StartInfo = _StartInfo}
    AddHandler _process.OutputDataReceived, AddressOf OutputHandler
    AddHandler _process.ErrorDataReceived, AddressOf ErrorHandler
    AddHandler _process.Exited, AddressOf Exited
    Try
      aSuccess = Await AwaitProcess()
    Catch ex As Exception
    End Try
    _bwConvertMedia.ReportProgress(ai)
  Next

这里是

Private Shared Async Function AwaitProcess() As Task(Of Integer)
  _tcs = New TaskCompletionSource(Of Integer)
  _status.Converting = True
  _Error.Clear()
  _process.Start()
  _process.BeginErrorReadLine()
  _process.BeginOutputReadLine()    
  Return Await _tcs.Task
End Function

问题是,当执行 Await _tcs.Task 时,会执行 _bwConvertMedia RunWorkerCompleted 过程,所以当我调用 _bwConvertMedia.ReportProgress(ai)

我收到一个错误消息,工作人员已经完成。

这是为什么?你能帮帮我吗?

发生的事情是

正确的行为是后台工作程序调用 100 次进程,然后完成执行并调用 RunWorkerCompleted

我对之前链接的代码进行了一些修改,这里有两个示例,分别是顺序非阻塞 Async/Await 过程和使用 Task.Factory.[=58= 的非阻塞并行过程]

Since I can't test your program, I simply used Tracert.exe to simulate a stdout result to update the User Interface.

为了将 运行 tasks/threads 与 UI 同步,我在第一种情况下使用进程的 .SynchronizingObject,在第二种情况下使用 TaskScheduler 方法 TaskScheduler.FromCurrentSynchronizationContext().

Tracert.exe 的输出被传递到两个文本框。 在并行示例中,我在任务之间插入了 1 秒的延迟,以查看两个文本框是如何更新的。

Async/Await 示例可以修改为不同的工作方式,因为您不需要等待一个任务完成才能开始另一个任务。

使用 List(Of ProcessStartInfo)List(Of Process)ProcessStartInfoProcess 对象添加到 Pool

These are used in both examples. Define a correct scope.

Public psInfoPool As List(Of ProcessStartInfo)
Public ProcessPool As List(Of Process)

顺序Async/Await

The delegate is used with SynchronizingObject.BeginInvoke if InvokeRequired = true

Public Delegate Sub UpdUI(_object As TextBox, _value As String)

Public Sub UpdateUIDelegate(control As TextBox, _input As String)
    control.AppendText(_input)
End Sub

    Dim NumberOfProcesses As Integer
    For x = 0 To 1
        Dim OutCtl As TextBox = If(x = 0, Me.TextBox1, Me.TextBox2)
        Dim _result As Integer = Await Task.Run(Async Function() As Task(Of Integer)
                                     Return Await Test_SequentialAsync("192.168.1.1", OutCtl)
                                 End Function)
        NumberOfProcesses += _result
    Next

The MediaToConvert parameter would be the name of the file to convert when you adapt the examples to your needs. The OutCtl parameter is just the TextBox used for the output

Public Async Function Test_SequentialAsync(ByVal MediaToConvert As String, OutCtl As TextBox) As Task(Of Integer)
    Dim _CurrentProcessInfo As Integer
    Dim _CurrentProcess As Integer

    Dim ExitCode As Integer = Await Task.Run(Function() As Integer

        Dim _processexitcode As Integer

        psInfoPool.Add(New ProcessStartInfo)
        _CurrentProcessInfo = psInfoPool.Count - 1
        psInfoPool(_CurrentProcessInfo).RedirectStandardOutput = True
        psInfoPool(_CurrentProcessInfo).CreateNoWindow = True
        psInfoPool(_CurrentProcessInfo).UseShellExecute = False
        'Name of the executable to start
        psInfoPool(_CurrentProcessInfo).FileName = "Tracert"    'psInfo.FileName = ".\mycmd.exe"""
        'Parameter(s) to pass to the executable
        psInfoPool(_CurrentProcessInfo).Arguments = MediaToConvert
        psInfoPool(_CurrentProcessInfo).WindowStyle = ProcessWindowStyle.Hidden

        ProcessPool.Add(New Process)
        _CurrentProcess = ProcessPool.Count - 1

        ProcessPool(_CurrentProcess) = New Process() With {.StartInfo = psInfoPool(_CurrentProcessInfo),
                                                           .EnableRaisingEvents = True,
                                                           .SynchronizingObject = Me}

        ProcessPool(_CurrentProcess).Start()
        ProcessPool(_CurrentProcess).BeginOutputReadLine()
        AddHandler ProcessPool(_CurrentProcess).OutputDataReceived,
            Sub(sender As Object, e As DataReceivedEventArgs)
                    If e.Data IsNot Nothing Then
                        If ProcessPool(_CurrentProcess).SynchronizingObject.InvokeRequired Then
                            ProcessPool(_CurrentProcess).SynchronizingObject.BeginInvoke(
                                                         New UpdUI(AddressOf UpdateUIDelegate),
                                                         New Object() {OutCtl,
                                                         e.Data + Environment.NewLine})
                        Else
                            OutCtl.AppendText(e.Data + Environment.NewLine)
                        End If
                    End If
            End Sub

        'Add an event handler for the Exited event
        AddHandler ProcessPool(_CurrentProcess).Exited,
                Sub(source As Object, ev As EventArgs)
                    _processexitcode = ProcessPool(_CurrentProcess).ExitCode
                    Console.WriteLine("The process has exited. Code: {0}  Time: {1}",
                    _processexitcode,
                    ProcessPool(_CurrentProcess).ExitTime)
                End Sub

        ProcessPool(_CurrentProcess).WaitForExit()
        ProcessPool(_CurrentProcess).Close()
        Return _processexitcode
    End Function)

    Return If(ExitCode = 0, 1, 0)

End Function

使用Task.Fatory

的并行进程

定义一个调度程序并将其与当前上下文相关联

Public _Scheduler As TaskScheduler = TaskScheduler.FromCurrentSynchronizationContext()

To use Await Task.Delay(1000) you must be in an Async method, but it's just for testing the output, it's not needed.

For x = 0 To 1
    Dim OutCtl As TextBox = If(x = 0, Me.TextBox1, Me.TextBox2)
    Dim _result As Integer = Test_ParallelTasks("192.168.1.1", OutCtl)
    Await Task.Delay(1000)
    NumberOfProcesses += _result
Next

Note that a new Task is created when the OutputDataReceived event handler reports that new data has been received. The UI is updated accordingly using DataReceivedEventArgs e.Data.

Private Function Test_ParallelTasks(ByVal MediaToConvert As String, OutCtl As TextBox) As Integer
    Dim _processexitcode As Integer
    Dim _CurrentProcessInfo As Integer
    Dim _CurrentProcess As Integer

    Task.Factory.StartNew(Function()
        psInfoPool.Add(New ProcessStartInfo)
        _CurrentProcessInfo = psInfoPool.Count - 1
        psInfoPool(_CurrentProcessInfo).RedirectStandardOutput = True
        psInfoPool(_CurrentProcessInfo).CreateNoWindow = True
        psInfoPool(_CurrentProcessInfo).UseShellExecute = False
        psInfoPool(_CurrentProcessInfo).FileName = "Tracert"  'psInfo.FileName = ".\mycmd.exe"
        psInfoPool(_CurrentProcessInfo).Arguments = MediaToConvert
        psInfoPool(_CurrentProcessInfo).WindowStyle = ProcessWindowStyle.Hidden

        ProcessPool.Add(New Process)
        _CurrentProcess = ProcessPool.Count - 1
        ProcessPool(_CurrentProcess) = New Process() With {.StartInfo = psInfoPool(_CurrentProcessInfo),
                                                           .EnableRaisingEvents = True,
                                                           .SynchronizingObject = Me}

        ProcessPool(_CurrentProcess).Start()
        ProcessPool(_CurrentProcess).BeginOutputReadLine()

        AddHandler ProcessPool(_CurrentProcess).OutputDataReceived,
            Sub(sender As Object, e As DataReceivedEventArgs)
                If e.Data IsNot Nothing Then
                    Try
                        'Update the UI or report progress 
                        Dim UpdateUI As Task = Task.Factory.StartNew(Sub()
                        Try
                            OutCtl.AppendText(e.Data + Environment.NewLine)
                        Catch exp As Exception
                              'An exception may raise if the form is closed
                        End Try

                        End Sub, CancellationToken.None, TaskCreationOptions.PreferFairness, _Scheduler)
                        UpdateUI.Wait()

                    Catch exp As Exception
                       'Do something here
                    End Try
                End If
            End Sub

        'Add an event handler for the Exited event
        AddHandler ProcessPool(_CurrentProcess).Exited,
                Sub(source As Object, ev As EventArgs)
                    _processexitcode = ProcessPool(_CurrentProcess).ExitCode
                    Console.WriteLine("The process has exited. Code: {0}  Time: {1}",
                    _processexitcode,
                    ProcessPool(_CurrentProcess).ExitTime)
                End Sub

        ProcessPool(_CurrentProcess).WaitForExit()
        ProcessPool(_CurrentProcess).Close()
        Return _processexitcode
    End Function, TaskCreationOptions.LongRunning, CancellationToken.None)

    Return If(_processexitcode = 0, 1, 0)
End Function