如何在Winform中使用多线程?

13

我是一个多线程的新手。我有一个包含标签和进度条的winform,想要显示处理结果。一开始我使用了Application.DoEvents()方法,但是发现窗体会冻结。后来我阅读了一些MSDN上关于多线程的文章。其次,我使用了BackgroundWorker来完成。

this.bwForm.DoWork += (o, arg) => { DoConvert(); };
this.bwForm.RunWorkerAsync();
表单在处理时无法冻结,以便我可以拖放。不幸的是,它抛出了一个InvalidOperationException异常。因此,我必须使用以下代码:Control.CheckForIllegalCrossThreadCalls = false; 确保这不是最终解决方案。
专家们,您有什么建议吗?
编辑:当我调用listview时,它会抛出InvalidOperationException异常。此代码位于DoWork()函数中。
          foreach (ListViewItem item in this.listView1.Items)
            {
                //........some operation
                 lbFilesCount.Text = string.Format("{0} files", listView1.Items.Count);
                 progressBar1.Value++;
            }

编辑2: 我在DoWork()方法中使用delegate和invoke,但它没有抛出异常。但窗体仍然会再次冻结。如何做到在处理时使窗体可拖放?


在DoConvert线程中你在做什么(例如,它是否正在操作任何GUI界面)? - Yet Another Geek
2
问题出现在DoConvert()函数内部。关闭检查只是隐藏问题,而不是解决问题。 - H H
您可能正在尝试从未创建它的线程访问控件。为此,您需要检查标签的invoke属性是否为true,如果是,则调用控件并更改其值。http://www.ureader.com/msg/144644873.aspx - stuartmclark
@又一个极客,@Henk Holterman,DoConvert()函数是对标签和进度条进行一些修改。 - Justin
你没有展示给我们委托、Invoke 或者 DoWork 的代码。你必须向我们展示代码以便我们进行诊断。 - Dour High Arch
9个回答

11

您只能从UI线程(WinForm线程)设置进度条用户控件属性。使用BackgroundWorker最简单的方法是使用ProgressChanged事件:

private BackgroundWorker bwForm;
private ProgressBar progressBar;

在WinForm构造函数中:
this.progressBar = new ProgressBar();
this.progressBar.Maximum = 100;
this.bwForm = new BackgroundWorker();
this.bwForm.DoWork += new DoWorkEventHandler(this.BwForm_DoWork);
this.bwForm.ProgressChanged += new ProgressChangedEventHandler(this.BwForm_ProgressChanged);
this.bwForm.RunWorkerAsync();

...

void BwForm_DoWork(object sender, DoWorkEventArgs e)
{
    BackgroundWorker bgw = sender as BackgroundWorker;
    // Your DoConvert code here
    // ...          
    int percent = 0;
    bgw.ReportProgress(percent);
    // ...
}

void BwForm_ProgressChanged(object sender, ProgressChangedEventArgs e)
{
    this.progressBar.Value = e.ProgressPercentage;
}

请看这里:http://msdn.microsoft.com/en-us/library/system.componentmodel.backgroundworker.aspx 编辑:
我之前误解了你的问题,我以为它是关于在工作期间显示进度条进度的。如果它是关于工作结果,请在BwForm_DoWork事件中使用e.Result。添加一个完成事件处理程序并管理结果即可。
this.bwForm.RunWorkerCompleted += new RunWorkerCompletedEventHandler(this.BwForm_RunWorkerCompleted);

...

private void BwForm_RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
    YourResultStruct result = e.Result as YourResultStruct;
    if (e.Error != null && result != null)
    {
        // Handle result here
    }
}

这段代码在哪个方法中?DoWork、ProgressChanged、Completed还是其他方法?您不需要在foreach循环中添加lbFilesCount.Text = string.Format("{0} files", listView1.Items.Count);。在添加1之前,请确保将Value重置为0并正确设置进度条的Max属性。最好将Max设置为预期的最大值,将Value设置为列表计数。 - Vince
抱歉,这是DoWork代码。我想展示处理过程。所以我必须这样做。异常不会在foreach中抛出,而是在foreach开始时。谢谢。 - Justin
你不能在 DoWork 代码中完成这个操作。请在 ProgressChanged 中完成(在 DoWork 中计算 e.ProgressPercentage)。 - Vince
我知道这个。但是……在DoWork中我有其他操作。我确实使用了ProgressChanged事件,但对我没有用。foreach(ListViewItem item in this.listView1.Items)引起异常。foreach (ListViewItem item in this.listView1.Items) {//... this.bwForm.ReportProgress(convertProgress); //lbFilesCount.Text = string.Format("{0} files", listView1.Items.Count); //progressBar1.Value++; //convertAmount++; } - Justin
ReportProgress 应该在 DoWork 和 WinForm 用户控件处理 ProgressChanged 中... 如果它不起作用,我不知道。您不能在 DoWork 的 foreach 中处理 listview,DoWork 不能引用 WinForm,如果需要参数,请通过后台工作器运行传递它们并在结果中获取它们。如果您需要比百分比更复杂的进度结构(我怀疑),则需要使用委托和跨线程调用。 - Vince
1
@Justin,BackgroundWorker 上有一个名为 WorkerReportsProgress 的属性,你必须将其设置为 true,否则它将永远不会触发 ProgressChanged 事件。 - Don Kirkby

8

调用UI线程。例如:

void SetControlText(Control control, string text)
{
    if (control.InvokeRequired)
        control.Invoke(SetControlText(control, text));
    else
        control.Text = text;
}

正如您建议的那样,我已经这样做了,而且效果很好。但是...表单又再次冻结了。我的意愿是在处理时可以流畅地拖动表单。 - Justin
@Justin,你的UI线程上还有其他阻塞操作吗? - foxy
@freedompeace,这里有一个标签,一个进度条和一个列表视图。我需要修改它们。 - Justin
1
@Justin - 根据您对原问题的评论,我认为您在做事情时是反过来的。您应该在DoWork方法中执行长时间运行的非UI操作,并在ProgressChanged回调中更新UI进度条/标签等。DoWork在后台线程上执行,而ProgressChanged回调在UI线程上执行。 - ScottTx
@ScottTx,你说得很有道理。但是我必须调用一个ListView来修改progressBar和label,这导致了InvalidOperationException异常。你似乎让我离最终解决方案更近了一步。+1 - Justin

6
避免调用 Application.DoEvents。通常情况下,此方法会导致更多问题。它被普遍认为是一种不良实践,因为存在更好的替代方法可以保持UI消息正在运行。
避免更改 CheckForIllegalCrossThreadCalls = false 设置。这样做并不能解决问题,只是掩盖了问题。问题是你正试图从主UI线程之外的线程访问UI元素。如果禁用了 CheckForIllegalCrossThreadCalls,那么您将不再收到异常,但应用程序会出现不可预测且惊人的失败。
DoWork 事件处理程序内,您需要定期调用 ReportProgress。从你的 Form,你需要订阅 ProgressChanged 事件。在 ProgressChanged 事件处理程序内部访问UI元素是安全的,因为它会自动调度到UI线程。

5
最清晰的方法是像你所做的那样使用BackGroundWorker
你只是错过了一些要点:
1.您不能在DoWork事件处理程序中访问表单元素,因为这会导致跨线程方法调用,应该在ProgressChanged事件处理程序中完成。
2.默认情况下,BackGroundWorker不允许报告进度,也不允许取消操作。当您将BackGroundWorker添加到代码中时,如果想要调用BackGroundWorkerReportProgress方法,则必须将BackGroundWorkerWorkerReportsProgress属性设置为true
3.如果需要允许用户取消操作,请将WorkerSupportsCancellation设置为true,并在DoWork事件处理程序中的循环中检查名为CancellationPending的属性。
希望对您有所帮助。

5

4
您展示的代码存在一些根本性问题。正如其他人所提到的,从后台线程调用Application.DoEvents()将无法得到您要求的结果。将缓慢的后台处理过程分离到一个BackgroundWorker中,并在UI线程中更新进度条。您正在从后台线程调用progressBar1.Value++,这是错误的。
永远不要调用Control.CheckForIllegalCrossThreadCalls = false,这只会隐藏错误。
如果您想从后台线程更新进度条,则必须实现一个ProgressChanged处理程序;您目前没有这样做。
如果您需要一个实现BackgroundWorker的示例,请参阅Code Project上的文章BackgroundWorker Threads

谢谢。但不适合我的问题。我怀疑也许我应该使用委托和跨线程调用来调用listview。+1 - Justin
我使用委托和调用。它起作用了。但是窗体又冻结了。你有什么建议吗?谢谢。 - Justin
这个例子是一个带有标签和进度条的WinForm,显示结果并且不会“冻结”。那么它为什么“不适合我的问题”呢? - Dour High Arch

3
我认为编写线程安全的应用程序是比较难理解和正确执行的事情之一。你真的需要学习一下这方面的知识。如果你是通过一本书学习C#,那就回过头去看看有没有关于多线程的章节。我是从Andrew Troelsen的书中学习的(最新的是Pro C# 2005 and the .NET 2.0 Platform),他直到第14章才涉及这个主题(所以很多人在那之前就放弃了阅读)。我来自嵌入式编程背景,在那里并发性和原子性也是问题,因此这不是.NET或Windows特定的问题。
这里的许多帖子都详细介绍了如何使用.NET提供的工具处理线程的机制。这些都是有价值的建议和必须学习的东西,但如果你先了解一下“理论”,会更有帮助。以跨线程问题为例。实际上正在发生的是UI线程具有一些内置的复杂性,检查您是否从不同的线程修改控件。微软的设计师意识到这是一个容易犯的错误,所以内置了保护措施。为什么它很危险?这需要您了解原子操作是什么。由于控件的“状态”无法在原子操作中更改,因此一个线程可能开始更改控件,然后另一个线程可能变为活动状态,从而使控件处于部分修改状态。现在,如果您告诉UI我不在乎,让我的线程修改控件,它可能会起作用。但是,当它不起作用时,你将会有一个非常难以找到的错误。你会让你的代码变得不可维护,并且跟随你的程序员会诅咒你的名字。
因此,UI很复杂并为您检查,您编写的类和线程没有。您必须了解哪些内容是原子的在您的类和线程中。你可能会导致你的线程“冻结”。最常见的应用程序冻结是死锁条件导致的。在这种情况下,“冻结”意味着UI永远不会再次响应。总之,这篇文章已经太长了,但我认为至少你应该熟悉“lock”的使用,也可能需要了解Monitor、Interlocked、Semaphore和Mutex。
只是为了给你一个想法:(来自Troelsen的书)
intVal++; //This is not thread safe 
int newVal = Interlocked.Increment(ref intVal); //This is thread safe

.NET还提供了[Synchronization]属性。这可以使编写线程安全类变得容易,但使用它会付出效率上的代价。我只是希望给你一些复杂性的想法,并激励你去做更进一步的阅读。


2

按照以下步骤操作,创建一个新的线程

         Thread loginThread = new Thread(new ThreadStart(DoWork));

         loginThread.Start();

在ThreadStart()方法中传递你想要执行的方法。如果在此方法内部想要更改某些控件属性,则创建一个委托并将其指向一个方法,在该方法中编写更改控件属性的代码。

         public delegate void DoWorkDelegate(ChangeControlsProperties);         

如果要调用控件属性,请按照以下方法操作:声明一个方法,并在其中定义控件的新属性。

         public void UpdateForm()
         {
             // change controls properties over here
         }

然后将代理指向该方法,以此方式实现。
         InvokeUIControlDelegate invokeDelegate = new InvokeUIControlDelegate(UpdateForm);

那么当您想在任何地方更改属性时,只需调用此函数:

         this.Invoke(invokeDelegate);

希望这段代码片段能帮到你!:)

1
为了补充freedompeace的解决方案,他是正确的。这个解决方案是“线程安全”的,这正是你想要的。你只需要使用BeginInvoke而不是Invoke即可。
void SetControlText(Control control, string text)
{
    control.BeginInvoke(
        new MethodInvoker(() =>
        {
            control.Text = text;
        })
    );
}

上述修复方法是最简单和最干净的。

希望这可以帮到你。:)


表格仍然冻结。我的程序要复杂得多。 - Justin
谢谢。我确实做了。但是表单仍然冻结。 - Justin
1
在线程内调用 SetControlText(control, string) 方法。例如:new Thread(() => { SetControlText(control, string); }) { IsBackground = true }.Start();。您可以自由编辑以结束线程。这仍然是一种安全的调用方法,它应该可以解冻您的表单。 - Nahydrin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接