(C#) BackgroundWorker() ProgressChanged 不工作
Posted
技术标签:
【中文标题】(C#) BackgroundWorker() ProgressChanged 不工作【英文标题】:(C#) BackgroundWorker() ProgressChanged not working 【发布时间】:2020-05-30 13:16:49 【问题描述】:我有一个 WPF 应用程序,它由两个线程组成,模拟企业在 52 周内生产和销售商品(每周只允许进行一次交易)。我还需要使用后台工作人员,以便可以在列表视图中显示数据。截至目前,我的 UI 在单击 simulate
时冻结,但我可以看到输出仍在调试终端中工作。我已经尝试了所有我能想到的方法,老实说,我得到了老师的帮助,甚至他也找不到有效的解决方案。
-
当我调用
Simulate()
时,我的 UI 冻结是什么原因?
当我的代码不同并且我的 UI 没有冻结时,我的列表视图永远不会更新,因为 DataProgress()
似乎不起作用 - e.UserStart
永远不会迭代。
模拟按钮调用:
private void Simulate(object sender, RoutedEventArgs e)
// Declare BackgroundWorker
Data = new ObservableCollection<Operations>();
worker = new BackgroundWorker();
worker.WorkerReportsProgress = true;
worker.WorkerSupportsCancellation = true;
worker.RunWorkerAsync(52);
worker.DoWork += ShowData;
worker.ProgressChanged += DataProgress;
worker.RunWorkerCompleted += DataToDB;
Production = new Production(qtyProduction, timeExecProd);
Sales = new Sales(qtySales, timeExecSales);
Thread prod = new Thread(Production.Product);
prod.Start();
Thread.Sleep(100);
Thread sales = new Thread(Sales.Sell);
sales.Start();
DoWork : ShowData() :
Console.WriteLine("Simulation started | Initial stock : 500");
Production = new Production(qtyProduction, timeExecProd);
Sales = new Sales(qtySales, timeExecSales);
while (Factory.Week < max) // max = 52
if (worker.CancellationPending) // also this isn't reacting to worker.CancelAsync();
e.Cancel = true;
// My teacher tried to call my threads from here, but it breaks the purpose of having
// two threads as he was just calling 52 times two functions back to back and therefore
// wasn't "randomizing" the transactions.
int progressPercentage = Convert.ToInt32(((double)(Factory.Week) / max) * 100);
(sender as BackgroundWorker).ReportProgress(progressPercentage, Factory.Week);
ProgressChanged : DataProgress() :
if (e.UserState != null) // While using debugger, it looks like this is called over & over
Data.Add(new Operations()
id = rnd.Next(1,999),
name = Factory.name,
qtyStock = Factory.Stock,
averageStock = Factory.AverageStock,
week = Factory.Week
);
listview.ItemsSource = Data;
RunWorkerCompleted : DataToDB() :
// Outputs "Work done" for now.
如果你想知道当我调用我的线程时会发生什么,它看起来像这样:
卖出():
while (Factory.Week <= 52)
lock (obj)
// some math function callings¸
Factory.Week++;
Thread.Sleep(timeExecSales);
我应该使用第三个线程来更新我的列表视图吗?我看不出我需要如何将它与我的静态变量同步。 这是我学习多线程的第一个项目......我有点无能为力,甚至我的老师都帮不上忙。
【问题讨论】:
正如发布的那样,while 循环永远不会终止。它以非常高的速度猛击 UI 线程,每秒数百万次。这也是非常昂贵的代码,它使 UI 线程消耗 100% 核心并且永远无法赶上。副作用是它不再执行其正常职责,重新绘制窗口并响应输入。鉴于这是多么错误,最好变得激烈。而是在 DoWork 中生成结果列表并使用 RunWorkerCompleted 更新 ItemsSource。 只看你的代码,在你钩住所有事件之前不要告诉后台工作人员开始运行。此外,仅更改控件上的 ItemSource 不会强制它刷新。至少,您可能应该在列表视图上调用 DataBind,并可能调用刷新/重绘。 正如 stacy 所说.. 将while (Factory.Week < max)
更改为 while (Factory.Week < max && !worker.CancellationPending)
尽管有其他问题,您应该为此使用任务。我最喜欢的编程名言之一来自 Stephen Cleary 的“C# Cookbook 中的并发”:“只要你输入 new Thread(),就结束了;你的项目已经有遗留代码了。”
【参考方案1】:
一方面,发布的代码中没有足够的上下文来全面了解您的问题准确。但是,我们可以仅从您发布的代码中推断出问题所在。
首先,让我们尝试回答您的两个问题。我们可能会推断出以下几点:
这里的代码:
if (e.UserState != null)
Data.Add(new Operations()
id = rnd.Next(1,999),
name = Factory.name,
qtyStock = Factory.Stock,
averageStock = Factory.AverageStock,
week = Factory.Week
);
listview.ItemsSource = Data;
您正在使用 Windows 窗体后台线程对象来尝试更新 WPF GUI 对象,该对象只能在主 GUI 线程上完成。还有一个明显的禁忌是永远不要从非 UI 线程更新 GUI 对象。使用BackgroundWorker
在线程(前台/后台)、上下文和执行方面也有其自身的问题,因为它依赖于Dispatcher
和SynchronizationContexts
来完成工作。
然后就是在这一行中一遍又一遍地设置绑定的好奇心:
listview.ItemsSource = Data;
让我们在里面放一个别针......
正如其他评论者已经指出的那样,您的 while 循环中没有退出策略:
while (Factory.Week < max) // max = 52
if (worker.CancellationPending) // also this isn't reacting to worker.CancelAsync();
e.Cancel = true;
// My teacher tried to call my threads from here, but it breaks the purpose of having
// two threads as he was just calling 52 times two functions back to back and therefore
// wasn't "randomizing" the transactions.
int progressPercentage = Convert.ToInt32(((double)(Factory.Week) / max) * 100);
(sender as BackgroundWorker).ReportProgress(progressPercentage, Factory.Week);
但这不是更大的问题......除了误用/误解何时/多少/如何使用线程之外,似乎没有任何类型的线程同步。无法以这种方式预测或跟踪生命周期的线程执行。
在这一点上,这个问题在技术上或多或少得到了回答,但我觉得这只会让你更加沮丧,并且不会比你开始时更好。因此,也许上一门关于基本设计的速成课程可能有助于理顺这个烂摊子,这是你的老师应该做的事情。
假设您正在从事软件开发,并且可以说在这里选择了 WPF 作为您的“面包板”,您可能会遇到诸如 MVC(模型视图控制器)或 MVVM(模型视图视图模型)之类的术语。您还可能会遇到设计原则,例如 SOLID、关注点分离以及将事物分组到服务中。
您的代码完美地说明了所有这些框架和原则存在的原因。让我们看看您遇到的一些问题以及如何解决它们:
您将线程代码(逻辑和服务 - 控制器 [松散地说])与演示代码(列表视图更新 - 视图)和集合更新(您的可观察集合 - 模型)混合在一起。这就是(许多)您在编码、修复和维护手头的问题时遇到如此困难的原因之一。要清理它,请将其分离出来(关注点分离)。您甚至可以将每个操作移动到其自己的类中,并使用该类的接口/API(服务/微服务)。
并非所有事情都需要用线程来解决。但是现在,让我们先学会爬行,然后再跑步。在你开始学习 async/await 或 TPL(任务并行库)之前,让我们回到老学校。找一本好书……甚至可以找到 20 年前的东西……走老路,学习如何使用 ThreadPool 和内核同步对象,例如互斥锁、事件等,以及如何在线程之间发出信号。一旦你掌握了,然后了解 TPL 和 async/await。
不要越过溪流。不要混用 WinForms、WPF,我什至看到了Console.WriteLine
。
了解数据绑定,尤其是它在 WPF 中的工作原理。 ObservableCollection
是你的朋友,将你的 ItemsSource
绑定到它一次,然后更新 ObservableCollection
并保留 GUI 对象。
希望这将帮助您理顺代码并使事情运行起来。
祝你好运!
【讨论】:
感谢您的时间和详细的回答。我不明白如何正确使用线程,因为这是我的老师给我们的第一个任务。他还刚刚向我们介绍了 MVC,所以这方面的知识也不多。我一直在 Java 中使用 MVC,但我觉得在 WPF 中做一个项目时更难使用它,它在我必须实现的所有新东西之上添加了另一层。 我的应用程序只是一个模拟器,用于计算我在工厂生产了多少产品以及销售了多少产品。每周只能发生一笔交易。我使用了一个静态变量和lock
,这样一次只有一个线程可以访问它。此外,当我的线程执行 52 次时,我需要实时更新列表视图,但当然如果我在主线程中这样做,我的应用程序将被冻结。这就是为什么我尝试添加一个后台工作程序并希望我的while
循环一旦达到最大值(52)就会结束。我的迭代应该可以工作,但 while 循环确实是
运行方式快速且不与线程同步。我只是不知道如何构建这样的应用程序,而且我的老师没有正确地教我们……我只是有一个带有静态变量的类 Factory。然后我有 Production
和 Sales
类是我的线程。
两个想法:首先查看 Interlocked.Increment,您可以完全避免 lock
,请参阅此示例:docs.microsoft.com/en-us/dotnet/api/… 其次,查看无锁技术,或者至少单个编写器多个读取器。你真的应该避免使用lock
语句,至少是你使用它的方式(如果你必须使用它,创建一个单例静态对象来锁定)。
我睡在上面,读了一些老师给我们的笔记。他还告诉我如何解决实际状态的代码,我想在大幅更改代码之前了解您的意见。他建议我应该将我的Production
和Sales
类编辑为BackgroundWorkers
。然后,我可以将进度结果返回到模型中,并使用主 (WPF) 线程更改我的视图。我想我会需要另一个线程来定期检查新结果以返回主线程的某种循环?以上是关于(C#) BackgroundWorker() ProgressChanged 不工作的主要内容,如果未能解决你的问题,请参考以下文章
C# form发起backgroundworker 当form close时 backgroundworker 还会继续工作吗