代码之家  ›  专栏  ›  技术社区  ›  Fiona - myaccessible.website

.NET中的多线程处理-在后台实现计数器

  •  1
  • Fiona - myaccessible.website  · 技术社区  · 15 年前

    作为一个培训练习,我正在创建一个在.NET3.5WPF应用程序的后台线程中运行的计数器。其思想是计数器递增,每隔几秒钟,总输出到屏幕。

    因为这是一个训练练习,前景线实际上不做任何事情,但是想象一下它做了!

    有一个启动和停止按钮来启动和停止计数器。

    问题是,我希望前台线程每隔几秒钟轮询一次总计,并在表单的文本框中更新它。当我尝试此操作时,整个应用程序都“没有响应”。目前,我只让它在开始计数后的固定时间更新计数。

    我把所有的代码都放在这里,这样你就可以得到整个画面:

     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 !

    6 回复  |  直到 15 年前
        1
  •  3
  •   Martin Brown    15 年前

    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);
        }
    }
    

    正如您所看到的,这意味着一个窗口一次只能处理一个消息/事件。当您在按钮单击事件中睡眠时,消息循环被阻塞,应用程序停止响应(获取和处理消息)。

    如果你使用 DispatcherTimer 它要求Windows定期在Windows消息队列中粘贴一条wm_计时器(tick)消息。这允许您的应用程序在不使用其他线程的情况下处理其他消息,同时仍然能够定期执行某些操作。

    有关更多详细信息,请参阅msdn“ About Messages and Message Queues

        2
  •  2
  •   Jorge Córdoba    15 年前

    它进入“无响应”状态,因为您使用的是循环。

    将循环替换为计时器,所有循环都将再次工作:p

    问题是您在主线程中使用了一个循环,这样它就永远不会退出click事件并使主线程保持繁忙状态。

    由于主线程在循环中很忙,它无法处理Windows消息(除其他外,这些消息用于处理窗口的输入和绘制)。每当Windows检测到应用程序没有处理消息时,它都会将其报告为“没有响应”。

        3
  •  2
  •   Jon Norton    15 年前

    当您将轮询放入循环中时,会发生的情况是,您永远不会让控制权返回给调度器,调度器为应用程序执行所有Windows消息处理(用于绘制、移动、最小化/最大化、键盘/鼠标输入等的消息)。这意味着您的应用程序没有响应Windows发送的任何消息,因此它被标为“没有响应”。

    有几种方法可以重组代码以获得所需的行为。正如其他人提到的,您可以在GUI线程上创建一个计时器来更新文本框。您可以通过创建 DispatcherTimer 设置 Tick Interval 性质。

    您还可以每次更新GUI ReportIncrement 通过呼叫呼叫 Control.Invoke 并传递一个更新文本框的函数。例如:

        private void ReportIncrement(int currentCount) 
        { 
            reportedCount = currentCount; 
            txtCurrentCount.Invoke(
                new Action(()=>txtCurrentCount.Text = reportedCount.ToString())
            );
        } 
    
        4
  •  1
  •   Navaar    15 年前

    您可以使用BackgroundWorker对象。它支持通过订阅的事件报告进度。这样就不必对后台任务的状态进行线程轮询。它非常容易使用,并且应该在应用程序中实现。它还可以和WPF调度器一起使用。

    至于为什么你的应用程序中会出现“没有响应”的消息。它是您的按钮按下处理程序中的thread.sleep()。这是告诉UI线程停止,这会使应用程序停止处理消息,从而触发“没有响应”消息。

        5
  •  1
  •   Darien Ford    15 年前

    虽然MartinBrown的示例确实有效,但是您的应用程序仍然会在与所有其他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);
                }
            }
        }
    }
    

    这是我认为更“老派”的线程,当然这取决于你对“老派”的定义。

    编辑:我可能会指出,马丁和我的例子在功能上是相同的。

        6
  •  0
  •   TLiebe    15 年前

    我会尝试一些不同的方法。不是让调用程序轮询工作线程,而是让工作线程定期引发一个事件以向调用线程报告。例如,在您的计数循环中,您可以检查当前计数是否可以被1000(或其他)平均整除,如果可以,则引发一个事件。您的调用程序可以捕获事件并更新显示。