.NET GUI在承载大量数据时会冻结

3
我是一名使用.NET框架的初级开发者。
我遇到了一个问题,当我的应用程序处理大量数据时,GUI会冻结。
我有一个网格和一种输出文本框来记录字符串。网格为每条消息预留了一行。
然后,应用程序接收消息,网格会更新对应消息所在行的单元格。同时,我会在文本框中写入有关该消息的信息。
例如,文本框可能会显示以下消息:
10:23:45 Message 1 arrived and the result is OK
10:23:45 Message 2 arrived and the result is OK
10:23:45 Message 3 arrived and the result is FAIL
10:23:45 Message 4 arrived and the result is OK
10:23:46 Message 5 arrived and the result is OK
....

网格将会是这样的:

MESSAGE_ID | RESULT  <------- HEADER
Message_1  | OK
Message_2  | FAIL
Message_3  | OK
Message_4  | OK
Message_5  | OK
Message_6  | Waiting
Message_7  | Waiting
....

问题在于当我在很短的时间内收到多条消息时,GUI会冻结,因为它一直在更新网格和文本框。它会冻结直到所有消息都到达并更新了网格和文本输出。
你知道是否有一种方法可以让GUI不会冻结吗?使用多个线程来更新GUI?
我认为这不是一个BackgroundWorker,因为GUI应该做这项工作,但也许我错了。
编辑1:
实际上我有两个线程:
1)主线程。它是GUI,并且拥有一个BlockingQueue。
private BlockingQueue _guiQueue = new BlockingQueue(1000);

2)线程1 它接收消息,收到消息后会进行一些操作,然后将结果排队并发送给GUI:

_guiQueue.Enqueue(new UpdateResult(_message.Name, _message.Result));

我正在使用阻塞队列,这些队列如下: http://www.codeproject.com/KB/recipes/boundedblockingqueue.aspx 主线程接收到消息后,只更新格子和输出文本框,不做其他事情。
    public MainThread(IMainForm mainView)
    {
        // presenter 
        _mainView = mainView;
        ....
    // Blocking queues
        _guiQueue = new BlockingQueue(1000);
        ....
        // Timer
        logger.Debug("Initializing Timer");
        _timer = new DispatcherTimer();
        _timer.Interval = TimeSpan.FromMilliseconds(10);
        // Call handleMessages method everytime the timer wakes up
         _timer.Tick += HandleMessages;
        _timer.Start();
        ...
        // Order Passing Thread
        logger.Debug("Launching OPThread");
        _orderPassingThread = new OPThread(_OPQueue, _commonObjects);
        _orderPassingThreadProcess = new Thread(new ThreadStart(_orderPassingThread.OPThreadProcess));
        _orderPassingThreadProcess.Start();
        ...
     }

    private void HandleMessages(Object sender, EventArgs args)
    {
        Presenter.Messages.Message message;

        while ((message = _guiQueue.Dequeue(10)) != null)
        {
            switch (message.MessageType)
            {
                case messageTypes.updateResult:
                    UpdateResult updateStepMsg = (UpdateResult) message;          
                    _mainView.updateStepResult(updateStepMsg.Name, updateStepMsg.Result); // updates             Grid and text box           
         break;
            ....
                default:
                    break;
            }
        }
    }

问题出现在我一秒钟内收到了超过一条消息。

例如,我有一个停止按钮可以停止所有操作,但是由于GUI冻结,无法单击该按钮。

谢谢!

PS:我正在使用DevExpress,网格是XtraGrid,输出文本框是memoEdit控件。


你能展示一下生成后台线程的代码吗?还有包含那个 while 循环的方法的全部内容吗? - Lasse V. Karlsen
如果这是Java,我可能会在_mainView.updateStepResult(updateStepMsg.Name, updateStepMsg.Result)上同步GUI线程策略。也许你可以从这里得到一些线索: https://dev59.com/53RB5IYBdhLWcg3wvpic - eee
“Any public static (Shared in Visual Basic) members of this type are thread safe. Any instance members are not guaranteed to be thread safe.” 这句话是什么意思?它是针对 DispatcherTimer 类的。参考链接:http://msdn.microsoft.com/en-us/library/system.windows.threading.dispatchertimer.aspx这位开发者在 http://beta.codeproject.com/KB/threads/WaitPulse.aspx 中提到了他的实现是安全的。 - eee
4个回答

2

在消息处理和GUI中常见的反模式是立即响应每个接收到的消息。更好的方法通常是将消息排队,然后仅在定时器(例如每250毫秒)上更新GUI。当您开始更新UI时,请使用高效的方法进行更新。许多专业UI组件具有“BeginUpdate \ EndUpdate”的概念,其中可以应用一批更改,而不会在应用每个更改时更新UI。

更新

你不应该使用ConcurrentQueue吗?BlockingQueue会阻塞读取器(即在您的情况下,看起来是UI),直到有可用数据。


@pedroruiz 那个链接对我来说无法使用,所以我无法查看那个阻塞队列的行为。如果你正在使用 .Net 4.0,尝试使用 ConcurrentQueue。 - Tim Lloyd
很不幸,我使用的是Net 3.5。 然而,如果那个BlockingQueue实现会阻塞读取器直到有可用数据,那么我的GUI不会一直冻结吗? 例如,如果每3秒钟只有一条消息到达,它就不会冻结。只有当每秒钟有多于1条消息时才会出现这种情况。 谢谢。 - pedroruiz
@pedroruiz 我明白了。是的,如果在队列为空时它阻塞读取器,那么当没有数据时你的GUI将会一直冻结。顺便说一下,你添加的队列实现链接仍然无效。 - Tim Lloyd
糟糕,这是代码 http://www.codeproject.com/KB/recipes/boundedblockingqueue.aspx - pedroruiz
@pedrorui 实现在队列为空时会阻塞读取器,但是您指定了超时时间,因此不会无限期地阻塞。我很好奇为什么您没有遇到任何QueueTimeoutException异常,因为假设有时当计时器触发时,队列中没有任何内容,因此您将超时? - Tim Lloyd

1
  1. 使用线程来进行后台计算。 (BackGroundWorker)
  2. 不要在每个新事件上更新屏幕,而是存储数据。运行一个定时器,以便每 X 秒钟将当前数据写入屏幕。屏幕需要改变才能让人们看到有东西进来,而不是显示完全最新的信息数百次每秒。

0

你的消息接收代码有多占用CPU资源?

尝试使用BackgroundWorker线程来处理消息接收,然后再更新GUI界面。

这个链接应该会对你有所帮助。

祝好运!


谢谢。我刚刚更新了帖子,添加了一些关于那个的信息。 - pedroruiz

0

我认为对你来说一个好的方法是使用XtraGrid教程中展示的解决方案。请查看Demos\Components\XtraGrid\CS\GridTutorials\GridVirtualData文件夹。它包含演示项目,展示如何有效地处理大量数据。在这个示例中,网格与包含100000条记录的集合一起快速工作。网格行由索引表示,并使用属性描述符获取实际值。因此,当网格加载时,它仅获取屏幕上可见的行。当网格滚动时,会动态访问其他数据。在IList.this[int fIndex]属性的getter中设置断点,你将看到网格有多聪明 :)

为了解冻GUI,可以使用Application.DoEvents()方法。

我看到你正在使用MemoEdit记录输入消息。它包含标准的多行文本框,但是这个控件在处理大量内容时速度非常慢 :(。如果我正确理解你的任务,你已经添加了编辑器以允许最终用户复制输入消息。如果我是你,我会用XtraGrid替换MemoEdit。它允许你从多个选定的记录中复制数据到剪贴板。

我们稍微修改了演示项目,这是我们最终得到的代码:

List<LogMessage> list = new List<LogMessage>();
for(int i = 0;i < 100000;i++) 
    list.Add(new LogMessage());
vList = new VirtualList(list);
grid.DataSource = vList;

...

    public class LogMessage {
        public LogMessage() {
            TimeStamp = DateTime.Now;
            Description = "Message at " + TimeStamp.Ticks.ToString();
        }
        public DateTime TimeStamp;
        public string Description;
    }

    public abstract class LogMessagePropertyDescriptor : PropertyDescriptor {
        bool fIsReadOnly;

        public LogMessagePropertyDescriptor(string fPropertyName, bool fIsReadOnly)
            : base(fPropertyName, null) {
            this.fIsReadOnly = fIsReadOnly;
        }

        public override bool CanResetValue(object component) { return false; }
        public override bool IsReadOnly {get { return fIsReadOnly; } }
        public override Type ComponentType { get { return typeof(LogMessage); } }
        public override void ResetValue(object component) {}
        public override bool ShouldSerializeValue(object component) { return true; }
    }
    public class LogMessageTimeStampPropertyDescriptor: LogMessagePropertyDescriptor {
        public LogMessageTimeStampPropertyDescriptor(bool fIsReadOnly)
            : base("TimeStamp", fIsReadOnly) {
        }
        public override Type PropertyType {get {return typeof(DateTime); } }
        public override object GetValue(object component) {
            LogMessage message = (LogMessage)component;
            return message.TimeStamp;
        }
        public override void SetValue(object component, object val) {
            LogMessage message = (LogMessage)component;
            message.TimeStamp = (DateTime)val;
        }
    }
    public class LogMessageDescriptionPropertyDescriptor: LogMessagePropertyDescriptor {
        public LogMessageDescriptionPropertyDescriptor(bool fIsReadOnly)
            : base("Description", fIsReadOnly) {
        }

        public override Type PropertyType { get { return typeof(string); } }

        public override object GetValue(object component) {
            LogMessage message = (LogMessage)component;
            return message.Description;
        }
        public override void SetValue(object component, object val) {
            LogMessage message = (LogMessage)component;
            message.Description = (string)val;
        }
    }
    public class VirtualList : IList, ITypedList {
        PropertyDescriptorCollection fColumnCollection;
        List<LogMessage> messages;

        public VirtualList(List<LogMessage> messages) {
            this.messages = messages;
            CreateColumnCollection();
        }
        public int RecordCount { get {return messages.Count; } }
        public int ColumnCount { get { return fColumnCollection.Count; } }
        protected virtual void CreateColumnCollection() {
            List<PropertyDescriptor> pds = new List<PropertyDescriptor>();
            pds.Add(new LogMessageTimeStampPropertyDescriptor(true));
            pds.Add(new LogMessageDescriptionPropertyDescriptor(false));
            fColumnCollection = new PropertyDescriptorCollection(pds.ToArray());
        }

        #region ITypedList Interface
        object IList.this[int fIndex] { get { return messages[fIndex]; } set { } }
        PropertyDescriptorCollection ITypedList.GetItemProperties(PropertyDescriptor[] descs) { return fColumnCollection; }
        string ITypedList.GetListName(PropertyDescriptor[] descs) { return ""; }
        #endregion
        #region IList Interface
        public virtual int Count { get { return RecordCount; } }
        public virtual bool IsSynchronized { get { return true; } }
        public virtual object SyncRoot { get { return true; } }
        public virtual bool IsReadOnly{ get { return false; } }
        public virtual bool IsFixedSize{ get { return true; } }
        public virtual IEnumerator GetEnumerator() { return null; }
        public virtual void CopyTo(System.Array array, int fIndex) {}
        public virtual int Add(object val) { throw new NotImplementedException(); }
        public virtual void Clear() { throw new NotImplementedException(); }
        public virtual bool Contains(object val) { throw new NotImplementedException(); }
        public virtual int IndexOf(object val) { throw new NotImplementedException(); }
        public virtual void Insert(int fIndex, object val) { throw new NotImplementedException(); }
        public virtual void Remove(object val) { throw new NotImplementedException(); }
        public virtual void RemoveAt(int fIndex) { throw new NotImplementedException(); }
        #endregion
    }

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