【问题标题】:Using Await in BackgroundWorker causes WorkerComplete event to be raised在 BackgroundWorker 中使用 Await 会引发 WorkerComplete 事件
【发布时间】:2018-06-12 15:54:18
【问题描述】:

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

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) 时

我收到一个错误,提示工人已经完成。

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

会发生什么

  • DoWork - 迭代 1
  • 在等待过程 1
  • RunWorkerComplete
  • DoWork 迭代 2-100

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

【问题讨论】:

  • 我的建议是使用Task.Factory(),如果这些任务可以并行运行,Task.Run(),如果你可以管理最终的并发(我不知道这些进程是做什么的)。当您安排任务时,无论如何,您都可以使用ContinueWith() 对其进行评估以测试状态并在.Status = TaskStatus.RanToCompletion 时对其进行详细说明。
  • 这些进程是编码进程,所以我没有优势让它们并行运行,因为它们使用 100% 的 GPU/CPU 来解码/编码视频。如果我和他们中的两个一起吃午饭,他们每个人只用 50%,时间加倍:)。 Jimi,谢谢你的建议,你有例子吗?
  • 这是一种特殊情况,因为您显然不直接管理编码过程,并且不清楚该过程是否同时受 CPU/GPU 和 IO 限制(首先在内存中编码,然后写入存储或同时存储)。看起来您正在使用 Background Worker 只是为了在尝试序列化进程输出时保持 UI 响应。但是,在这种情况下,我认为将多个并行进程调度到硬件 (CPU) 物理线程可能是一个想法。
  • 我几天前写了something like this 以及与starting a process and waiting its ending 相关的其他内容。这两个结合起来已经给你一个答案。但是,它们是用 C# 编写的。看看,让我知道。
  • 我整理了几个例子,以防你仍然需要它。

标签: vb.net process async-await task backgroundworker


【解决方案1】:

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

由于我无法测试您的程序,我只是使用Tracert.exe 来 模拟标准输出结果以更新用户界面。

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

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

可以将 Async/Await 示例修改为以不同的方式工作,因为您无需等待任务完成即可启动另一个任务。

使用List(Of ProcessStartInfo) 和List(Of Process) 将ProcessStartInfo 和Process 对象添加到池。

这两个例子都用到了。定义正确的范围。

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

顺序异步/等待

委托与 SynchronizingObject.BeginInvoke 一起使用,如果 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

MediaToConvert 参数将是文件名 当您根据需要调整示例时进行转换。 OutCtl 参数只是用于输出的 TextBox

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

要使用Await Task.Delay(1000),您必须使用异步方法,但是 只是为了测试输出,不需要。

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

请注意,OutputDataReceived 事件时会创建一个新任务 处理程序报告已收到新数据。用户界面已更新 相应地使用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

【讨论】:

  • 请注意,在Test_ParallelTasks() 方法中,我使用CancellationToken.None 作为参数。如果您需要以安全的方式取消线程,您可以创建一个 CancellationToken 并将其传递给方法。
猜你喜欢
  • 2013-10-11
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-08-31
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多