如何在不阻止UI的情况下等待任务完成

时间:2013-04-09 06:21:08

标签: .net vb.net task

这是我在这个论坛上的第一个问题,因为我不是母语为英语的人,所以我希望你能顺利对我说话,以防万一我做错或说错了。

所以,这是我的问题:

我喜欢解决一个简单的问题(看起来似乎如此),但我已经研究了一个多星期并尝试了不同的东西,但到目前为止还没有任何工作,我总是偶然发现同样的问题。

我喜欢做的是打开进度表单,然后启动两个长时间运行的任务(例如从两个数据库中获取数据)。这些长时间运行的任务在完成进度表单关闭后会向进度表单报告进度,并打开另一个表单以显示两个长时间运行的任务的结果。在进度表关闭并且程序继续(或关闭)之前,这两个任务都必须完成(或取消/失败)。

因此,进度表单(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

  If Me.InvokeRequired Then
    Me.Invoke(New ShowProgressDelegate(AddressOf Me.AddMessage1), New Object() {message})
  Else
    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

  If Me.InvokeRequired Then
    Me.Invoke(New ShowProgressDelegate(AddressOf Me.AddMessage2), New Object() {message})
  Else
    Me.ListBox2.Items.Add(message)
    Me.ListBox2.SelectedIndex = Me.ListBox2.Items.Count - 1
    Application.DoEvents()
  End If
End Sub

End Class

然后我有我的TestClass模拟长时间运行的任务并引发进度事件:

Imports System.Threading

Public Class TestClass

  Public Event ShowProgress(ByVal message As String)

  Private _milliSeconds As UShort

  Public Sub New(milliSeconds As UShort)
    _milliSeconds = milliSeconds
  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)
    testClass(1) = New TestClass(100)

    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(0), testTask(1))

        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

如果我将常量MULTI_THREAD设置为false,则一切正常(但顺序)。但是如果我将if设置为true,它只显示表单,但没有进度,它也不会显示生成的消息框。 如果我调试它永远不会到达Task.WaitAll(...)之后的行。

我已经尝试了其他方法来实现多线程,比如使用普通线程(没有返回值),后台工作者(只有百分比进度,没有文本消息),代理上的BeginInvoke / EndInvoke,但没有任何工作或它显示了相同的行为,如描述上方。

我在另一个论坛上被告知,Task.WaitAll会阻止UI线程,因为它是在UI线程上调用的,但我无法相信,因为MSDN明确指出Task.WaitAll等待移交完成或失败的任务,我没有移交UI线程。

所以,我不知道还有什么可以尝试的,所以我希望你能指出我的错误,或者告诉我另一种方法来解决我的小问题。

非常感谢你。

更新
我做了一些测试和调试。如果我将表单代码中的两个“Me.Invoke(...)”更改为“Me.BeginInvoke(...)”,那么我的表单中仍然没有显示进度消息,但至少任务有效然后我收到了关闭消息框。因此,实际传递或显示进度消息似乎会导致问题。

也许这会引发你的想法?

再次感谢你。

更新II:
经过大量的反复试验后,它似乎起作用了,至少有一点点。在创建任务时,我移交了“TaskScheduler.FromCurrentSynchronizationContext”,而不是我的表单代码中的“Me.InvokeRequired”部分。所以我至少显示了我的进度消息,但任务仍然没有完全并行运行。但这是我下一个问题的主题。

3 个答案:

答案 0 :(得分:0)

尝试将Task更改为BackgroundWorker。这个类是为表单创建的,并且有很好的api来显示关于你工作的表单上的信息。在这里查看tutorial

<强>更新 完整的例子,因为在评论中它看起来很难看:

public partial class Form1 : Form
{
    private BackgroundWorker bw = new BackgroundWorker();

    public Form1()
    {
        InitializeComponent();

        bw.WorkerReportsProgress = true;
        bw.WorkerSupportsCancellation = true;
        bw.DoWork += new DoWorkEventHandler(bw_DoWork);
        bw.ProgressChanged += new ProgressChangedEventHandler(bw_ProgressChanged);
        bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(bw_RunWorkerCompleted);
    }

    void bw_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
    {
        if ((e.Cancelled == true))
        {
            this.tbProgress.Text = "Canceled!";
        }

        else if (!(e.Error == null))
        {
            this.tbProgress.Text = ("Error: " + e.Error.Message);
        }

        else
        {
            this.tbProgress.Text = "Done!";
        }
    }

    void bw_ProgressChanged(object sender, ProgressChangedEventArgs e)
    {
        this.tbProgress.Text = (string)e.UserState;            
    }

    void bw_DoWork(object sender, DoWorkEventArgs e)
    {
        var worker = sender as BackgroundWorker;

        for (int i = 1; (i <= 10); i++)
        {
            if ((worker.CancellationPending == true))
            {
                e.Cancel = true;
                break;
            }
            else
            {
                // Perform a time consuming operation and report progress.
                System.Threading.Thread.Sleep(500);
                worker.ReportProgress((i * 10), "status you want "+i.ToString());
            }
        }
    }
}

答案 1 :(得分:0)

您的进程可能会占用系统端的大量资源。 它只是启动新任务,尝试为重流程启动完整的新线程

您甚至可以在单独的线程中启动进度UI,以便它不会停止响应

Thread nThread;
private void Button1_Click(System.Object sender, System.EventArgs e)
{
    nThread = new Thread(ShowDlg);
    nThread.Start();
}

private void ShowDlg()
{
    Form1 nWin = new Form1();
    nWin.ShowDialog();
}

答案 2 :(得分:0)

启动任务后,您正在调用WaitAll,这将阻止UI线程,因此您的进度将无法显示。

您应该指定一个回调或继续任务,以便在任务完成后执行,并在那里执行最终的操作,例如显示结果并关闭进度表。

Task.Factory.ContinueWhenAll(tasks, Sub(antecedents)
  result(0) = testTask(0).Result
  result(1) = testTask(1).Result
  RemoveHandler testClass(0).ShowProgress, AddressOf frm.AddMessage1
  RemoveHandler testClass(1).ShowProgress, AddressOf frm.AddMessage2

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

我通常不是vb.net程序员,因此您可能需要稍微调整一下以确保闭包工作正常并将这些调用中的一些调用回到ui线程