为什么我的任务不并行运行?

时间:2013-04-11 06:53:46

标签: .net vb.net multithreading task

我整理了一个小测试程序,以了解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

识别TestClass

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)时,反之亦然。它显示了" Run 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来电与#34;在列表"消息不会显示在调试窗口中。

我用Google搜索了InvokeRequired部分的正确方法,我做的方式似乎是正确的,但它不起作用。
有人可以告诉我,有什么不对吗?

再次感谢您的帮助。

2 个答案:

答案 0 :(得分:2)

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

TaskScheduler.FromCurrentSynchronizationContext

在这种情况下,该同步上下文是UI线程 - 其中只有一个。因此,任务将排队,直到它们可以在UI线程上运行 - 这只有在您输入WaitAll()调用时才可用(因为WaitAll的规则说,如果当前线程适合运行一个或多个任务(它是)和那些相同的任务尚未启动(它们不能拥有),当前线程可以直接执行其中一个或多个任务)

可以要求在不同的同步上下文(或线程池)上运行任务,但是你会遇到一个不同的问题 - 在事件中线程正在提升,你正在与UI元素进行交互。

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

答案 1 :(得分:0)

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

在我看来,Task.WaitAll()不仅要等待移交任务完成,还要阻止调用它的任务。
所以,从

更改我的主程序的代码
...
Task.WaitAll(testTask)
...

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

这个技巧和我的进度消息会显示在列表框中。