作为一项训练练习,我在.NET 3.5 WPF应用程序中创建一个在后台线程中运行的计数器。这个想法是计数器增加,每隔几秒就将总数输出到屏幕。
由于这是一个训练练习,前台线程并没有真正做任何事情,但想象一下!
有一个开始和停止按钮来启动和停止计数器。
问题是我希望前台线程每隔几秒轮询一次总数并在表单上的文本框中更新它。当我尝试这个时,整个应用程序都会“无响应”。目前我只能在启动它之后的一段固定时间内更新计数。
我会将所有代码转储到这里,以便您全面了解:
public partial class Window1 : Window
{
public delegate void FinishIncrementing(int currentCount);
private int reportedCount;
private Thread workThread;
public Window1()
{
InitializeComponent();
}
#region events
private void Window_Loaded(object sender, RoutedEventArgs e)
{
txtCurrentCount.Text = "0";
reportedCount = 0;
InitialiseThread();
}
private void btnStart_Click(object sender, RoutedEventArgs e)
{
workThread.Start();
// Do some foreground thread stuff
Thread.Sleep(200); // I want to put this in a loop to report periodically
txtCurrentCount.Text = reportedCount.ToString();
}
private void btnStop_Click(object sender, RoutedEventArgs e)
{
workThread.Abort();
txtCurrentCount.Text = reportedCount.ToString();
InitialiseThread();
}
#endregion
private void InitialiseThread()
{
WorkObject obj = new WorkObject(ReportIncrement, reportedCount);
workThread = new Thread(obj.StartProcessing);
}
private void ReportIncrement(int currentCount)
{
reportedCount = currentCount;
}
}
public class WorkObject
{
private Window1.FinishIncrementing _callback;
private int _currentCount;
public WorkObject(Window1.FinishIncrementing callback, int count)
{
_callback = callback;
_currentCount = count;
}
public void StartProcessing(object ThreadState)
{
for (int i = 0; i < 100000; i++)
{
_currentCount++;
if (_callback != null)
{
_callback(_currentCount);
Thread.Sleep(100);
}
}
}
}
我的问题是:我如何定期报告,以及为什么当我尝试将报告置于for循环中时它会失败?
编辑:应该提到我的训练是为了熟悉老式的多线程方法,所以我不能使用BackgroundWorker
!
答案 0 :(得分:3)
Windows应用程序中的每个表单都有一个如下所示的消息循环:
while (NotTimeToCloseForm)
{
Message msg = GetMessageFromWindows();
WndProc(msg);
}
这适用于所有Windows应用程序,而不仅仅是.Net应用程序。 WPF提供了WinProc的默认实现,如下所示:
void WndProc(Message msg)
{
switch (msg.Type)
{
case WM_LBUTTONDOWN:
RaiseButtonClickEvent(new ButtonClickEventArgs(msg));
break;
case WM_PAINT:
UpdateTheFormsDisplay(new PaintEventArgs(msg));
break;
case WM_xxx
//...
break;
default:
MakeWindowsDealWithMessage(msg);
}
}
正如您所看到的,这意味着一个窗口一次只能处理一个消息/事件。当你在Button Click事件中睡觉时,消息循环被阻止,应用程序停止响应(获取和处理消息)。
如果使用DispatcherTimer,则要求Windows定期在Windows消息队列中粘贴WM_TIMER(Tick)消息。这允许您的应用程序处理其他消息,同时仍然可以定期执行某些操作而无需使用其他线程。
有关详细信息,请参阅MSDN“About Messages and Message Queues”
答案 1 :(得分:2)
因为您正在使用循环,所以“无响应”。
将循环替换为计时器,所有这些都将再次起作用:P
问题是你在主线程中使用循环,所以它永远不会退出click事件并保持主线程忙。
由于主线程忙于循环,它无法处理Windows消息(用于处理窗口输入和绘图的其他事物)。每当Windows检测到应用程序未处理消息时,它都会将其报告为“无响应”。
答案 2 :(得分:2)
当您将轮询置于循环中时发生的事情是,您永远不会让控制权返回到执行所有Windows消息处理的Dispatcher(用于绘制,移动,最小化/最大化,键盘/鼠标输入的消息)等...应用程序。这意味着您的应用程序没有响应Windows发送的任何消息,因此将其标记为“无响应”。
有几种方法可以重构代码以获得所需的行为。正如其他人所提到的,您可以在GUI线程上创建一个更新文本框的计时器。您可以通过创建DispatcherTimer
并设置Tick
和Interval
属性来实现此目的。
您还可以通过调用ReportIncrement
每次调用Control.Invoke
进行GUI更新,并向其传递更新文本框的函数。例如:
private void ReportIncrement(int currentCount)
{
reportedCount = currentCount;
txtCurrentCount.Invoke(
new Action(()=>txtCurrentCount.Text = reportedCount.ToString())
);
}
答案 3 :(得分:1)
您可以使用BackgroundWorker对象。它支持通过您订阅的事件报告进度。这样,您就不必对后台任务的状态进行线程轮询。它非常易于使用,在您的应用程序中应该很容易实现。它也适用于WPF调度员。
至于为何您的应用中出现“无响应”消息。它是您在按钮按下处理程序中的Thread.Sleep()。这告诉UI线程停止,这使得应用程序停止处理消息,这会触发“无响应”消息。
答案 4 :(得分:1)
虽然Martin Brown的示例确实有效,但您的应用程序仍会在与所有其他Windows消息相同的队列中收到WM_TICK消息,从而使其同步。
作为一个替代,这里是一个“真正的多线程”的例子,你的进程产生另一个线程,线程调用更新委托来更新标签的值。
public partial class Window1 : Window
{
Thread _worker;
public Window1()
{
InitializeComponent();
_worker = new Thread(new ThreadStart(this.DoWork));
_worker.Start();
}
private void DoWork()
{
WorkObject tWorker = new WorkObject((MyUpdateDelegate)delegate (int count) { this.UpdateCount(count); } , 0);
tWorker.StartProcessing();
}
public delegate void MyUpdateDelegate (int count);
private void UpdateCount(int currentCount)
{
if (Dispatcher.CheckAccess())
{
lblcounter.Content = currentCount;
}
else
{
lblcounter.Dispatcher.Invoke((MyUpdateDelegate)delegate (int count)
{
this.UpdateCount(count);
}, currentCount);
}
}
protected override void OnClosing(System.ComponentModel.CancelEventArgs e)
{
if (_worker != null)
{
_worker.Abort();
}
base.OnClosing(e);
}
}
public class WorkObject
{
private Window1.MyUpdateDelegate _callback;
private int _currentCount;
public WorkObject(Window1.MyUpdateDelegate callback, int count)
{
_callback = callback;
_currentCount = count;
}
public void StartProcessing()
{
for (int i = 0; i < 100000; i++)
{
_currentCount++;
if (_callback != null)
{
_callback(_currentCount);
Thread.Sleep(100);
}
}
}
}
这就是我认为更“老派”的线索,当然这取决于你对“老派”的定义。
编辑:我可能会指出Martin和我的例子在功能上是一样的。
答案 5 :(得分:0)
我会尝试一些不同的东西。而不是调用程序线程的调用程序,让工作线程定期引发事件以报告回调用线程。例如,在计数循环中,您可以检查当前计数是否可以被1000(或其他)整除,如果是,则引发事件。您的调用程序可以捕获事件并更新显示。