1

我整理了一个小测试程序来了解 VB.net 中的多线程。经过大量的研究、尝试和失败后,我的程序至少运行了一点。

现在它的行为很奇怪,我无法解释原因。也许你可以告诉我,我的程序有什么问题,为什么它的行为方式和/或我必须改变什么才能让它按照它应该的方式工作。

我的程序包含一个类(TestClass),它模拟了我希望每个在不同线程上执行的长时间运行的任务,一个进度表(Form1),用于显示来自我的长时间运行的任务的进度消息和主程序,这两个一起。

这是我的代码:

进度表(Form1) 包含两个列表框(ListBox1 和 ListBox2):

Public Class Form1

Public Sub AddMessage1(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Me.ListBox1.Items.Add(message)
  Me.ListBox1.SelectedIndex = Me.ListBox1.Items.Count - 1
  Application.DoEvents()
End Sub

Public Sub AddMessage2(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Me.ListBox2.Items.Add(message)
  Me.ListBox2.SelectedIndex = Me.ListBox2.Items.Count - 1
  Application.DoEvents()
End Sub

End Class

测试类

Imports System.Threading

Public Class TestClass

  Public Event ShowProgress(ByVal message As String)

  Private _milliSeconds As UShort
  Private _guid As String

  Public Sub New(milliSeconds As UShort, ByVal guid As String)
    _milliSeconds = milliSeconds
    _guid = guid
  End Sub

  Public Function Run() As UShort
    For i As Integer = 1 To 20
      RaiseEvent ShowProgress("Run " & i)
      Thread.Sleep(_milliSeconds)
    Next i

    Return _milliSeconds
  End Function

End Class

主要程序

Imports System.Threading.Tasks
Imports System.ComponentModel

Public Class Start
  Private Const MULTI_THREAD As Boolean = True

  Public Shared Sub Main()
    Dim testClass(1) As TestClass
    Dim testTask(1) As Task(Of UShort)
    Dim result(1) As UShort

    testClass(0) = New TestClass(50, "Test1")
    testClass(1) = New TestClass(200, "Test2")

    Using frm As Form1 = New Form1
      frm.Show()

      AddHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      AddHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      If MULTI_THREAD Then
        testTask(0) = Task(Of UShort).Factory.StartNew(Function() testClass(0).Run, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext)
        testTask(1) = Task(Of UShort).Factory.StartNew(Function() testClass(1).Run, CancellationToken.None, TaskCreationOptions.None, TaskScheduler.FromCurrentSynchronizationContext)

        Task.WaitAll(testTask)

        result(0) = testTask(0).Result
        result(1) = testTask(1).Result
      Else
        result(0) = testClass(0).Run
        result(1) = testClass(1).Run
      End If

      RemoveHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      RemoveHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      frm.Close()
    End Using

    MessageBox.Show("Result 1: " & result(0) & "; Result 2: " & result(1))
  End Sub

End Class

该程序应该打开进度表,并行运行两个长时间运行的任务,显示两个长时间运行的任务的进度消息,当两个长时间运行的任务完成时关闭进度表并显示一个消息框,显示结果长期运行的任务。

现在我无法解释的行为:如果我运行这个程序,它会在两个列表框中显示“运行 1”,所以两个任务都开始运行,但是只有第一个列表框被填充并且只有当第一个列表框被填充时(第一个任务完成),第二个列表框继续填充。因此,两个任务都开始了,但第二个任务等待第一个任务完成,然后再继续运行。
但是,当我在我的主要程序中注释掉时Task.WaitAll(testTask),情况正好相反。它在两个列表框中显示“运行 1”,然后第二个列表框被填充,只有当第二个列表框被填充(第二个任务完成)时,第一个列表框继续运行。

为什么我的程序表现得如此奇怪,我怎样才能让它同时运行我的两个任务?

感谢您帮助解决这个小谜团:-)


更新
由于到目前为止唯一的答案表明它与SynchronizationContext我正在使用的有关,因此我将其从主程序的代码中删除:

主要程序

Imports System.Threading.Tasks
Imports System.ComponentModel

Public Class Start
  Private Const MULTI_THREAD As Boolean = True

  Public Shared Sub Main()
    Dim testClass(1) As TestClass
    Dim testTask(1) As Task(Of UShort)
    Dim result(1) As UShort

    testClass(0) = New TestClass(50, "Test1")
    testClass(1) = New TestClass(200, "Test2")

    Using frm As Form1 = New Form1
      frm.Show()

      AddHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      AddHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      If MULTI_THREAD Then
        testTask(0) = Task(Of UShort).Factory.StartNew(Function() testClass(0).Run)
        testTask(1) = Task(Of UShort).Factory.StartNew(Function() testClass(1).Run)

        Task.WaitAll(testTask)

        result(0) = testTask(0).Result
        result(1) = testTask(1).Result
      Else
        result(0) = testClass(0).Run
        result(1) = testClass(1).Run
      End If

      RemoveHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
      RemoveHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

      frm.Close()
    End Using

    MessageBox.Show("Result 1: " & result(0) & "; Result 2: " & result(1))
  End Sub

End Class

为了避免InvalidOperationException由于非法的跨线程调用,我将表单的代码更改如下:

进度表(Form1) 包含两个列表框(ListBox1 和 ListBox2):

Public Delegate Sub ShowProgressDelegate(ByVal message As String)

Public Class Form1

Public Sub AddMessage1(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Debug.Print("out List 1: " & message)

  If Me.InvokeRequired Then
    Me.BeginInvoke(New ShowProgressDelegate(AddressOf Me.AddMessage1), message)
  Else
    Debug.Print("in List 1: " & message)

    Me.ListBox1.Items.Add(message)
    Me.ListBox1.SelectedIndex = Me.ListBox1.Items.Count - 1
    Application.DoEvents()
  End If
End Sub

Public Sub AddMessage2(ByVal message As String)
  If String.IsNullOrEmpty(message) Then
    Exit Sub
  End If

  Debug.Print("out List 2: " & message)

  If Me.InvokeRequired Then
    Me.BeginInvoke(New ShowProgressDelegate(AddressOf Me.AddMessage2), message)
  Else
    Debug.Print("in List 2: " & message)

    Me.ListBox2.Items.Add(message)
    Me.ListBox2.SelectedIndex = Me.ListBox2.Items.Count - 1
    Application.DoEvents()
  End If
End Sub

End Class

我刚刚添加了一些 Debug.Prints 用于调试和InvokeRequired部件。

现在这两个任务并行运行(我知道是因为Debug.Print带有“out List”消息的调用)但我的进度消息没有显示在列表框中,相应的代码永远不会被执行(Debug.Print带有“in List”消息的调用不会' t 显示在调试窗口中)。

我用谷歌搜索了做这InvokeRequired部分的正确方法,我做的方法似乎是正确的,但它不起作用。
有人可以告诉我,怎么了?

再次感谢你的帮助。

4

2 回答 2

2

您是在告诉系统在当前同步上下文中运行任务。

TaskScheduler.FromCurrentSynchronizationContext

在这种情况下,同步上下文是 UI 线程——其中只有一个。所以任务将排队,直到它们可以在 UI 线程上运行 - 这只有在您进入WaitAll()调用时才可用(因为规则WaitAll说,如果当前线程适合运行一个或多个任务(它是)并且那些相同的任务还没有开始(他们不能拥有),当前线程可能直接执行一个或多个这些任务)

可以要求在不同的同步上下文(或线程池)上运行任务,但随后会遇到不同的问题 - 在线程引发的事件中,您正在与 UI 元素进行交互。

也许您应该查看BackgroundWorker该类,它在单独的线程上运行代码,但专门用于将进度报告回 UI/前台线程。

于 2013-04-11T07:04:20.700 回答
0

经过大量搜索和尝试,我想出了一个解决方案。它不漂亮,但它有效。

在我看来, Task.WaitAll() 不仅等待移交的任务完成,而且还会阻止它调用的任务。
因此,将我的主要程序的代码从

...
Task.WaitAll(testTask)
...

...
Do While Not Task.WaitAll(testTask, 100)
  Application.DoEvents
Loop
...

诀窍和我的进度消息是否显示在列表框中。

于 2013-04-18T07:31:38.020 回答