VB.Net 多个后台工作人员 - 仅最后一个任务完成

发布于 2024-10-15 00:13:25 字数 2739 浏览 3 评论 0原文

我一直在努力让它发挥作用。如果我在调试器中单步执行代码,一切都会很好。

我的问题是,如果我只是运行它,只有最后一个任务响应。我猜我正在覆盖后台工作或其他东西。我确信我做错了一些事情,但我的代码现在很混乱,因为我在搜索时尝试了很多方法。我知道线程池和 .Net 4.0 任务,但很难完成我需要的事情。

基本上我正在编写一个程序(更有可能尝试),该程序获取计算机列表和 ping,然后检查它们的正常运行时间并返回报告。

这在 UI 线程中工作得很好(显然这会锁定我的屏幕)。我可以让后台工作人员执行此操作,但随后它会逐一执行每台计算机,并且虽然屏幕响应良好,但仍然需要很长时间。

所以我的答案是为每个服务器启动一个新的后台工作线程有一个 for 循环。我的解决方案不起作用。

我已经看到其他线程可以做到这一点,但我需要与事件一起使用来调用代码以在每个线程完成时更新 UI。

最简单的方法是什么?

这是我的代码。大多数只是复制粘贴+修改,直到我让它正常工作。

所以在主类中我有测试人员。

(我尝试使用 Testworker() 但它说我无法使用Events 做到这一点)

当我单击按钮时,列表会加载。

Private WithEvents TestWorker As System.ComponentModel.BackgroundWorker

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
    Button1.IsEnabled = False

    Dim indexMax As Integer
                    indexMax = DataGridStatus.Items.Count
    For index = 1 To (indexMax)
        Dim Temp As ServerInfo = DataGridStatus.Items(index - 1)
        Temp.Index = index - 1
        Call_Thread(Temp)
    Next
End Sub


Private Sub Call_Thread(ByVal server As ServerInfo)
    Dim localserver As ServerInfo = server

    TestWorker = New System.ComponentModel.BackgroundWorker
    TestWorker.WorkerReportsProgress = True
    TestWorker.WorkerSupportsCancellation = True
    TestWorker.RunWorkerAsync(localserver)

End Sub

Private Sub TestWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestWorker.DoWork

    Dim iparray As IPHostEntry
    Dim ip() As IPAddress

    Dim Server As ServerInfo
    Server = e.Argument
    Try
        'Get IP Address first
        iparray = Dns.GetHostEntry(Server.ServerName)
        ip = iparray.AddressList
        Server.IPAddress = ip(0).ToString

        'Try Pinging
        Server.PingResult = PingHost(Server.ServerName)
        If Server.PingResult = "Success" Then

            'If ping success, get uptime
            Server.UpTime = GetUptime(Server.ServerName)
        Else
            Server.PingResult = "Failed"
        End If

    Catch ex As Exception
        Server.PingResult = "Error"
    End Try

    TestWorker.ReportProgress(0, Server)
    Thread.Sleep(1000)

End Sub


Private Sub TestWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles TestWorker.ProgressChanged

    Dim index As Integer
    Dim serverchange As ServerInfo = DirectCast(e.UserState, ServerInfo)

    index = DataGridStatus.Items.IndexOf(serverchange)
    ' index = serverchange.Index
    DataGridStatus.Items.Item(index) = serverchange

    ' ProgressBar1.Value = e.ProgressPercentage
    DataGridStatus.Items.Refresh()
End Sub

I have been pulling my hair out trying to get this to work. If I step through the code in debugger it all works great.

My problem is if I just run it, only the last task responds. I'm guessing I am overwriting the background working or something. I am sure I am doing a few things wrong but my code is now messy as I tried many way while searching. I know of the threadpool and .Net 4.0 tasks but having a hard time getting to do what I need.

Basicly I am writing a program (trying more likely) that takes a list of computers and pings then, then checks their uptime and reports back.

This works fine in the UI thread (Obviously that locks up my screen). I can have the background worker just do this, but then it does each computer 1 by one, and while the screen is responsive it still takes a long time.

So my answer was to have a for loop for each server launching a new background worker thread. My solution does not work.

I have seen other threads that I could do it, but I need to use with events to call code to update to UI when each is done.

What is the most simple way to do this?

Here is my code. Most is just copy paste + modify till I get it working right.

So In the main class I have the testworker.

(I tried using Testworker() but it said I could not do that WithEvents)

When I click the button the list loads.

Private WithEvents TestWorker As System.ComponentModel.BackgroundWorker

Private Sub Button1_Click(ByVal sender As System.Object, ByVal e As System.Windows.RoutedEventArgs) Handles Button1.Click
    Button1.IsEnabled = False

    Dim indexMax As Integer
                    indexMax = DataGridStatus.Items.Count
    For index = 1 To (indexMax)
        Dim Temp As ServerInfo = DataGridStatus.Items(index - 1)
        Temp.Index = index - 1
        Call_Thread(Temp)
    Next
End Sub


Private Sub Call_Thread(ByVal server As ServerInfo)
    Dim localserver As ServerInfo = server

    TestWorker = New System.ComponentModel.BackgroundWorker
    TestWorker.WorkerReportsProgress = True
    TestWorker.WorkerSupportsCancellation = True
    TestWorker.RunWorkerAsync(localserver)

End Sub

Private Sub TestWorker_DoWork(ByVal sender As Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles TestWorker.DoWork

    Dim iparray As IPHostEntry
    Dim ip() As IPAddress

    Dim Server As ServerInfo
    Server = e.Argument
    Try
        'Get IP Address first
        iparray = Dns.GetHostEntry(Server.ServerName)
        ip = iparray.AddressList
        Server.IPAddress = ip(0).ToString

        'Try Pinging
        Server.PingResult = PingHost(Server.ServerName)
        If Server.PingResult = "Success" Then

            'If ping success, get uptime
            Server.UpTime = GetUptime(Server.ServerName)
        Else
            Server.PingResult = "Failed"
        End If

    Catch ex As Exception
        Server.PingResult = "Error"
    End Try

    TestWorker.ReportProgress(0, Server)
    Thread.Sleep(1000)

End Sub


Private Sub TestWorker_ProgressChanged(ByVal sender As Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles TestWorker.ProgressChanged

    Dim index As Integer
    Dim serverchange As ServerInfo = DirectCast(e.UserState, ServerInfo)

    index = DataGridStatus.Items.IndexOf(serverchange)
    ' index = serverchange.Index
    DataGridStatus.Items.Item(index) = serverchange

    ' ProgressBar1.Value = e.ProgressPercentage
    DataGridStatus.Items.Refresh()
End Sub

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(4

琴流音 2024-10-22 00:13:25

您只能得到最后的结果,因为每次调用 TestWorker = New System.ComponentModel.BackgroundWorker 时,都会让您的 BackgroundWorker 崩溃。由于处理是异步完成的,因此在前面的工作完成之前,在 for 循环中会多次调用此行。

像下面这样的东西可能会起作用。 (抱歉,我的 VB 很生疏;可能有更有效的方式来表达这一点。)

Delegate Function PingDelegate(ByVal server As String) As String

Private _completedCount As Int32
Private ReadOnly _lockObject As New System.Object
Dim _rnd As New Random
Private _servers As List(Of String)

Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles GoButton.Click
    _servers = New List(Of System.String)(New String() {"adam", "betty", "clyde", "danny", "evan", "fred", "gertrude", "hank", "ice-t", "joshua"})
    _completedCount = 0
    ListBox1.Items.Clear()
    GoButton.Enabled = False
    BackgroundWorker1.RunWorkerAsync(_servers)
End Sub

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    Dim servers As List(Of System.String) = DirectCast(e.Argument, List(Of System.String))
    Dim waitHandles As New List(Of WaitHandle)

    For Each server As System.String In servers
        ' Get a delegate for the ping operation. .Net will let you call it asynchronously
        Dim d As New PingDelegate(AddressOf Ping)

        ' Start the ping operation async. When the ping is complete, it will automatically call PingIsDone
        Dim ar As IAsyncResult = d.BeginInvoke(server, AddressOf PingIsDone, d)

        ' Add the IAsyncResult for this invocation to our collection.
        waitHandles.Add(ar.AsyncWaitHandle)
    Next

    ' Wait until everything is done. This will not block the UI thread because it is happening
    ' in the background. You could also use the overload that takes a timeout value and
    ' check to see if the user has requested cancellation, for example. Once all operations
    ' are complete, this method will exit scope and the BackgroundWorker1_RunWorkerCompleted 
    ' will be called.
    WaitHandle.WaitAll(waitHandles.ToArray())
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    ListBox1.Items.Add(String.Format("{0} ({1}% done)", e.UserState, e.ProgressPercentage))
End Sub

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
    GoButton.Enabled = True
End Sub

Private Function Ping(ByVal server As System.String) As System.String
    ' Simulate a ping with random result and duration
    Threading.Thread.Sleep(_rnd.Next(1000, 4000))
    Dim result As Int32 = _rnd.Next(0, 2)
    If result = 0 Then
        Return server & " is ok"
    Else
        Return server & " is down"
    End If
End Function

Private Sub PingIsDone(ByVal ar As IAsyncResult)
    ' This method is called everytime a ping operation completes. Note that the order in which
    ' this method fires is completely independant of the order of the servers. The first server
    ' to respond calls this method first, etc. This keeps optimal performance.
    Dim d As PingDelegate = DirectCast(ar.AsyncState, PingDelegate)

    ' Complete the operation and get the result.
    Dim pingResult As String = d.EndInvoke(ar)

    ' To be safe, we put a lock around this so that _completedCount gets incremented atomically
    ' with the progress report. This may or may not be necessary in your application.
    SyncLock (_lockObject)
        _completedCount = _completedCount + 1
        Dim percent As Int32 = _completedCount * 100 / _servers.Count
        BackgroundWorker1.ReportProgress(percent, pingResult)
    End SyncLock
End Sub

You are only getting the last result because you are blowing away your BackgroundWorker each time you call TestWorker = New System.ComponentModel.BackgroundWorker. Since the processing is being done asynchronously, this line is being called multiple times within your for loop before the previous work has finished.

Something like the following might work. (Sorry, my VB is rusty; there are probably more efficient ways of expressing this.)

Delegate Function PingDelegate(ByVal server As String) As String

Private _completedCount As Int32
Private ReadOnly _lockObject As New System.Object
Dim _rnd As New Random
Private _servers As List(Of String)

Private Sub GoButton_Click(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles GoButton.Click
    _servers = New List(Of System.String)(New String() {"adam", "betty", "clyde", "danny", "evan", "fred", "gertrude", "hank", "ice-t", "joshua"})
    _completedCount = 0
    ListBox1.Items.Clear()
    GoButton.Enabled = False
    BackgroundWorker1.RunWorkerAsync(_servers)
End Sub

Private Sub BackgroundWorker1_DoWork(ByVal sender As System.Object, ByVal e As System.ComponentModel.DoWorkEventArgs) Handles BackgroundWorker1.DoWork
    Dim servers As List(Of System.String) = DirectCast(e.Argument, List(Of System.String))
    Dim waitHandles As New List(Of WaitHandle)

    For Each server As System.String In servers
        ' Get a delegate for the ping operation. .Net will let you call it asynchronously
        Dim d As New PingDelegate(AddressOf Ping)

        ' Start the ping operation async. When the ping is complete, it will automatically call PingIsDone
        Dim ar As IAsyncResult = d.BeginInvoke(server, AddressOf PingIsDone, d)

        ' Add the IAsyncResult for this invocation to our collection.
        waitHandles.Add(ar.AsyncWaitHandle)
    Next

    ' Wait until everything is done. This will not block the UI thread because it is happening
    ' in the background. You could also use the overload that takes a timeout value and
    ' check to see if the user has requested cancellation, for example. Once all operations
    ' are complete, this method will exit scope and the BackgroundWorker1_RunWorkerCompleted 
    ' will be called.
    WaitHandle.WaitAll(waitHandles.ToArray())
End Sub

Private Sub BackgroundWorker1_ProgressChanged(ByVal sender As System.Object, ByVal e As System.ComponentModel.ProgressChangedEventArgs) Handles BackgroundWorker1.ProgressChanged
    ListBox1.Items.Add(String.Format("{0} ({1}% done)", e.UserState, e.ProgressPercentage))
End Sub

Private Sub BackgroundWorker1_RunWorkerCompleted(ByVal sender As System.Object, ByVal e As System.ComponentModel.RunWorkerCompletedEventArgs) Handles BackgroundWorker1.RunWorkerCompleted
    GoButton.Enabled = True
End Sub

Private Function Ping(ByVal server As System.String) As System.String
    ' Simulate a ping with random result and duration
    Threading.Thread.Sleep(_rnd.Next(1000, 4000))
    Dim result As Int32 = _rnd.Next(0, 2)
    If result = 0 Then
        Return server & " is ok"
    Else
        Return server & " is down"
    End If
End Function

Private Sub PingIsDone(ByVal ar As IAsyncResult)
    ' This method is called everytime a ping operation completes. Note that the order in which
    ' this method fires is completely independant of the order of the servers. The first server
    ' to respond calls this method first, etc. This keeps optimal performance.
    Dim d As PingDelegate = DirectCast(ar.AsyncState, PingDelegate)

    ' Complete the operation and get the result.
    Dim pingResult As String = d.EndInvoke(ar)

    ' To be safe, we put a lock around this so that _completedCount gets incremented atomically
    ' with the progress report. This may or may not be necessary in your application.
    SyncLock (_lockObject)
        _completedCount = _completedCount + 1
        Dim percent As Int32 = _completedCount * 100 / _servers.Count
        BackgroundWorker1.ReportProgress(percent, pingResult)
    End SyncLock
End Sub
葵雨 2024-10-22 00:13:25

更新:我发布这个答案的重点是从技术角度来看您正在尝试做的事情(使用许多后台工作人员),而没有真正考虑这是否是完成您的<的好方法。 em>真实目标。事实上,我认为您可以使用单个 BackgroundWorker 和类似 Parallel.ForEach 在其 DoWork 事件处理程序中循环(这可以处理很多具体工作,例如 戴夫的解决方案)。


当您在 VB 中声明 WithEvents TestWorker As BackgroundWorker 时,它会将其包装成类似这样的内容(不完全正确 - 这只是为了说明这个想法):

Private _TestWorker As BackgroundWorker
Private Property TestWorker As BackgroundWorker
    Get
        Return _TestWorker
    End Get
    Set(ByVal value As BackgroundWorker)
        ' This is all probably handled in a more thread-safe way, mind you. '

        Dim prevWorker As BackgroundWorker = _TestWorker
        If prevWorker IsNot Nothing Then
            RemoveHandler prevWorker.DoWork, AddressOf TestWorker_DoWork
            ' etc. '
        End If

        If value IsNot Nothing Then
            AddHandler value.DoWork, AddressOf TestWorker_DoWork
            ' etc. '
        End If
        _TestWorker = value
    End Set
End Property

当您意识到这一点时,很明显,通过设置 TestWorker 到一个new BackgroundWorker 在每次调用 Call_Thread 时,您将从中删除任何附加的处理程序该字段先前引用的对象。

最明显的修复方法就是在每次调用 Call_Thread 时创建一个新的 local BackgroundWorker 对象,并在那里附加处理程序(使用 AddHandlerRemoveHandler),然后让它做它的事情:

Private Sub Call_Thread(ByVal server As ServerInfo)
    Dim localserver As ServerInfo = server

    ' Use a local variable for the new worker. '
    ' This takes the place of the Private WithEvents field. '
    Dim worker As New System.ComponentModel.BackgroundWorker

    ' Set it up. '
    With worker
        .WorkerReportsProgress = True
        .WorkerSupportsCancellation = True
    End With

    ' Attach the handlers. '
    AddHandler worker.DoWork, AddressOf TestWorker_DoWork
    AddHandler worker.ProgressChanged, AdressOf TestWorker_ProgressChanged

    ' Do the work. '
    worker.RunWorkerAsync(localserver)
End Sub

只要从 UI 线程中创建工作线程就应该没问题,因为 BackgroundWorker 自动附加到其构造函数中的当前SynchronizationContext(如果我没记错的话)。

Update: I posted this answer focusing on exactly what you were trying to do from a technical standpoint (use many background workers) without really putting much thought into whether or not this was a good way to accomplish your real objective. In fact, I think you could achieve what you're going for much more easily with a single BackgroundWorker and something like a Parallel.ForEach loop in its DoWork event handler (this takes care of a lot of the nitty gritty work in, e.g., Dave's solution).


When you declare WithEvents TestWorker As BackgroundWorker in VB it wraps it up something like this (not exactly—this is just to illustrate the idea):

Private _TestWorker As BackgroundWorker
Private Property TestWorker As BackgroundWorker
    Get
        Return _TestWorker
    End Get
    Set(ByVal value As BackgroundWorker)
        ' This is all probably handled in a more thread-safe way, mind you. '

        Dim prevWorker As BackgroundWorker = _TestWorker
        If prevWorker IsNot Nothing Then
            RemoveHandler prevWorker.DoWork, AddressOf TestWorker_DoWork
            ' etc. '
        End If

        If value IsNot Nothing Then
            AddHandler value.DoWork, AddressOf TestWorker_DoWork
            ' etc. '
        End If
        _TestWorker = value
    End Set
End Property

When you realize this, it becomes clear that by setting TestWorker to a new BackgroundWorker on every call to Call_Thread, you are removing any attached handlers from the object previously referenced by the field.

The most obvious fix would simply be to create a new local BackgroundWorker object in each call to Call_Thread, attach the handlers there (using AddHandler and RemoveHandler), and then just let it do its thing:

Private Sub Call_Thread(ByVal server As ServerInfo)
    Dim localserver As ServerInfo = server

    ' Use a local variable for the new worker. '
    ' This takes the place of the Private WithEvents field. '
    Dim worker As New System.ComponentModel.BackgroundWorker

    ' Set it up. '
    With worker
        .WorkerReportsProgress = True
        .WorkerSupportsCancellation = True
    End With

    ' Attach the handlers. '
    AddHandler worker.DoWork, AddressOf TestWorker_DoWork
    AddHandler worker.ProgressChanged, AdressOf TestWorker_ProgressChanged

    ' Do the work. '
    worker.RunWorkerAsync(localserver)
End Sub

Creating the worker right there in the method should be fine as long as you do so from the UI thread, since BackgroundWorker automatically attaches to the current SynchronizationContext in its constructor (if I remember correctly).

此刻的回忆 2024-10-22 00:13:25

理想情况下,您应该只使用 1 个后台工作程序,并像这样使用它:

  • 组装所有需要完成的工作:在您的情况下,是 ServerInfo 列表
  • 在后台执行工作: ping 所有服务器并保留结果
  • 报告进度:例如每个服务器 ping 后将
  • 结果放回 DoWorkEventArgs.Result
  • 将结果显示回 UI 中。

Ideally you should use only 1 backgroundworker and use it like this:

  • Assemble all the work that needs to be done: in your case a list of ServerInfo
  • Do the work in the background: ping all the servers and keep the result
  • Report progress: for example after each server pinged
  • Put results back in DoWorkEventArgs.Result
  • Display the results back in your UI.
一片旧的回忆 2024-10-22 00:13:25

您需要将 TestWorker_DoWorkTestWorker_ProgressChanged 附加到 Call_Thread 内的 DoWorkProgressChanged 事件。我还没有检查其余的代码,但这就是为什么它现在没有做任何事情。

TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
AddHandler TestWorker.DoWork, AddressOf TestWorker_DoWork
AddHandler TestWorker.ProgressChanged, AddressOf TestWorker_ProgressChanged
TestWorker.RunWorkerAsync(localserver)

You need to attach TestWorker_DoWork and TestWorker_ProgressChanged to the DoWork and ProgressChanged events within Call_Thread. I haven't yet examined the rest of the code, but that is why it isn't doing anything now.

TestWorker = New System.ComponentModel.BackgroundWorker
TestWorker.WorkerReportsProgress = True
TestWorker.WorkerSupportsCancellation = True
AddHandler TestWorker.DoWork, AddressOf TestWorker_DoWork
AddHandler TestWorker.ProgressChanged, AddressOf TestWorker_ProgressChanged
TestWorker.RunWorkerAsync(localserver)
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文