当调用UI元素时,线程运行缓慢

4

我正在编写一个基准测试工具,它在一个线程中从本地服务器读取一些变量。

            int countReads = 1000;

            Int64 count = 0;

            for (int i = 0; i < countReads; i++)
            {
                Thread.CurrentThread.Priority = ThreadPriority.Highest;
                DateTime start = DateTime.Now;

                session.Read(null, 0, TimestampsToReturn.Neither, idCollection, out ReadResults, out diagnosticInfos);

                DateTime stop = DateTime.Now;
                Thread.CurrentThread.Priority = ThreadPriority.Normal;

                TimeSpan delay = (stop - start);

                double s = delay.TotalMilliseconds;
                count += (Int64)s;

                Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
                {
                    progressBar1.Value = i;
                }));
            }

            double avg = (double)count / countReads;

            Dispatcher.Invoke(DispatcherPriority.Input, new Action(() =>
            {
                listBox1.Items.Add(avg);
            }));

我正在计算读取所需的时间,并在最后得到平均时间跨度。
            DateTime start = DateTime.Now;

            session.Read(null, 0, TimestampsToReturn.Neither, idCollection, out ReadResults, out diagnosticInfos);

            DateTime stop = DateTime.Now

如果我在不更新进度条的情况下运行代码,平均需要大约5毫秒。但是如果我加上进度条,运行时间会更长。

            Dispatcher.Invoke(DispatcherPriority.Render, new Action(() =>
            {
                progressBar1.Value = i;
            }));

平均需要大约10毫秒。

我的问题是,为什么使用进度条时时间跨度更高?我只是计算阅读的时间跨度,不包括进度条更新。

有没有办法避免UI绘制,以便它不会影响我的阅读时间跨度?

谢谢你的帮助。

最好的问候


1
额外的5毫秒对于一个简单的上下文切换、属性设置和发出一些绘制消息来说似乎是很长的时间。GUI消息队列很忙吗?无论如何,尝试使用BeginInvoke,这样你就不必等待了。 - Martin James
1
使用 Stopwatch 而不是 DateTime.Now - Marat Khasanov
BeginInvoke是相同的行为。我在想,因为如果我用Thread.sleep(20)替换读取请求,时间跨度几乎精确为20毫秒。无论是否使用进度条,但有没有办法更新UI而不会使读取请求的时间跨度更长? - Destructor
@Marat:我之前用过计时器。行为一样。 - Destructor
1
session.read() - 这个调用是否有可能需要或生成GUI消息呢(现在只是试探)?可怕的想法 - 没有COM STA,有吗? - Martin James
4个回答

12

停止使用 Invoke 在 UI 线程中传递进度信息。将进度信息发布到共享数据结构或变量中,并使用定时器在合理的时间间隔内轮询它。我知道似乎我们都被洗脑了,认为 Invoke 是处理工作者与 UI 线程交互的万能方法,但对于简单的进度信息,它可能是(而且经常是)最糟糕的方法。

使用 UI 线程上的定时器进行轮询方法具有以下优点。

  • 它打破了 Invoke 对 UI 和工作者线程的紧密耦合。
  • UI 线程可以决定何时以及如何频繁地更新进度信息,而不是反过来。当你停下来思考一下,这本来就应该是正确的方式。
  • 您会获得更高的吞吐量,包括 UI 和工作者线程。

我知道这并没有直接回答你关于为什么 session.Read 似乎运行较慢的问题。尝试从推模型(通过 Invoke)更改为拉模型(通过定时器)来更新进度信息的策略。看看是否有所改善。即使没有,我仍会因为上面列出的原因坚持使用拉模型。


1
这听起来是一种更好的策略。一个简单的例子会非常有帮助。我也会尝试自己做。 - Stonetip

2
这段文字的意思是:MSDN对 Dispatcher.Invoke的解释如下:

在与Dispatcher相关联的线程上同步地执行指定的委托。

基本上,Dispatcher.Invoke会阻塞,直到调度程序线程处理完请求。
建议使用Dispatcher.BeginInvoke

如果当前线程与Dispatcher相关联,那么这就是有意义的。 - sll
我尝试了BeginInvoke,得到了相同的行为。如果线程在我调用调度程序的部分被阻塞,那也没关系。我只是希望读取请求的部分能够无延迟地工作。如果线程因为更新GUI而被阻塞,它不应该影响读取的时间跨度,对吗? - Destructor
好的。尝试测量执行10000次读取所用的时间,并将结果除以10000,以获得更好的平均读取时间。逐个测量读取可能是您得到如此无关紧要的结果的原因。 - Nicolas Repiquet

1

我来晚了9年,但我认为这是一个更简单的解决方案:只需等待进度条值达到某个阈值,然后再更新它。在我的示例中,我每五分之一最大值刷新一次工具栏。

private static int progressBarMaxValue = -1;
private static int progressBarChunkSize = -1;

        public static void progressBarSetNotRealTimeValue(ProgressBar progressBar, int argNewValue)
        {
            if (progressBarMaxValue != -1)
            {
                if (argNewValue < progressBarChunkSize)
                {
                    //Threshold not reached yet, discard the new value.
                    return;
                }
                else
                {
                    //Allow the update, and set the next threshold higher.
                    progressBarChunkSize += progressBarChunkSize;
                }
            }

            if (Thread.CurrentThread.IsBackground)
            {
                progressBar.BeginInvoke(new Action(() =>
                {
                    if (progressBarMaxValue == -1)
                    {
                        progressBarMaxValue = progressBar.Maximum;
                        progressBarChunkSize = progressBar.Maximum / 5;
                    }

                    progressBar.Value = argNewValue;
                }));
            }
            else
            {
                progressBar.Value = argNewValue;
            }
        }

0
如果当前执行的线程与您使用的Dispatcher相关联,则Invoke()将阻塞此线程,在这种情况下,请尝试使用Dispatcher.BeginInvoke()异步完成任务。

MSDN,Dispatcher.Invoke方法:

调用是同步操作;因此,在回调返回之前,控制不会返回到调用对象。

顺便说一下,尝试一下DispatcherPriority.Send


我之前使用过计时器。行为相同。 - Destructor
BeginInvoke怎么样?Invoke会阻塞与Dispatcher相关联的线程,因此如果当前线程关联,则会出现不必要的超时。 - sll
OP已经尝试过了。我很困惑为什么Invoke/BeginInvoke会影响session.read()调用的时间。OP甚至提高了读取线程的优先级! - Martin James
在Invoke/BeginInvoke中传递的DispatcherPriority与更改CurrentThread.Priority不同。 - sll

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