Mono的BackgroundWorker不在后台工作?

4

我在使用Mono时遇到了问题。我试图理解为什么BackgroundWorker实例不会并发运行,或者至少不是非常高效或快速的。它似乎是线程数量被某种方式限制了,但我不是一个很好的Mono开发者或.NET开发者,所以我向我的Stack Overflow兄弟们求助。

考虑以下示例:

using System;
using System.ComponentModel;
using System.Threading;

namespace backgroundworkertest
{
  class MainClass
  {
    public static void foo(object sender, DoWorkEventArgs e) {
      Console.WriteLine("Start");
      Thread.Sleep(500);
      Console.WriteLine("Done");
    }

    public static void Main (string[] args)
    {
      for(int i = 0; i < 6; i++) {
        BackgroundWorker b = new BackgroundWorker();
        b.WorkerReportsProgress = false;
        b.DoWork += new DoWorkEventHandler(foo);
        b.RunWorkerAsync();
      }

      Thread.Sleep(2000);
      Console.WriteLine("Really done");
    }
  }
}

当我运行上述代码时,输出的结果如下。
开始
开始
完成
开始
开始
完成
开始
完成
真正完成
首先需要说明的是,并非所有的BackgroundWorker都能在两秒钟内完成。从循环中可以看出,我实例化了6个workers,但只有3个打印出“Done”。如果所有6个workers同时运行,则我认为两秒钟应该足够它们全部完成。
事实上,我有一个应用程序,在C# .NET中运行良好(使用大量的BackgroundWorker在后台获取图像)。然而,问题在于我将应用程序移植到Mono平台后,仅有少数几个BackgroundWorker同时运行,因此HTTP连接容易超时。因此,我想进行上述示例并询问以下问题...
这里发生了什么?为什么不是所有的BackgroundWorker同时运行?是否需要设置某些参数?

2
在任何人问之前,我知道我应该使用线程池。 ;) - Tom
1个回答

9

对于 Mono 的实现,我并不百分之百确定,但我很了解 Microsoft 版本,我只能假设 Mono 的行为方式相同或类似。

首先,需要澄清的是,BackgroundWorker确实会生成 .NET 线程(与本机线程不同),就好像你自己调用 ThreadPool.QueueUserWorkItem 一样。这是一个可能有助于理解为什么会看到这种行为的细节。需要记住的重要一点是,在 .NET 中,线程实际上可能并不表示本机线程。即使在运行6个后台工作程序时,使用6个.NET线程,也可以所有后台工作程序都在1-2个本机线程上运行。在我的计算机上,只有3个本机线程实际上被生成了。

其次,您没有考虑构造新的BackgroundWorker,设置委托,然后异步运行该工人所需的时间,这可能涉及创建本机线程,这实际上是一个相当昂贵的操作。通过添加定时器,在我的测试中,创建所有六个工作程序总共需要略微超过一秒钟的时间,有时会稍长一些。由于调度,ThreadPool 在发现所有线程饱和/阻塞(因为Thread.Sleep)之前可能需要几毫秒的时间,然后创建新线程。这意味着,在某些情况下,您的后台工作程序可能会在0:01:700左右开始,但只剩下300毫秒的时间,而该函数会休眠500毫秒。这个时间不足以完成任务。

我认为没有人会否认 Mono 的实现运行得比 Microsoft 的慢一些,这也是你可能没有发现可以通过他们的运行时获取相同行为的原因。

当你不确定时,请责怪你自己的代码,因为问题通常就在那里。按照你预期的操作用多长时间休眠,并不是测试某些东西的好方法。如果计算机上有其他程序正在运行会怎样?如果其他进程的线程抢占了你的线程,Thread.Sleep并不会变慢。在这种情况下,看起来效果更差。

如果你将代码更改为在foo方法中休眠5ms,并在创建后台工作程序后的主方法中等待20ms,就像你等待的时间比例一样,我能够看到以下结果:

开始
开始
完成
完成
开始
开始
真正完成
按任意键继续. . .
只有两个完成了,而且甚至没有全部开始。我希望你现在能看到测试中的缺陷。它们是并发运行的,我猜你的问题可能出在其他地方。你说你正在后台通过http获取图像。请记住,如果某些东西没有正确工作,我会说只有1%的时间实际上是运行时出了问题,另外99%的时间是你的代码。你使用同步还是异步I/O?只有一个线程处理请求吗?我会从那里开始。
编辑:
我决定看看Mono的性能实际上是什么样子的,而不仅仅依赖于我在Microsoft实现中找到的内容。在发现Mono实际上启动了所有6个后台工作之后;它们都在2000ms限制内完成了!为了记录,这里是结果的比较。我稍微修改了一下代码以显示正在进行的情况,但我向您保证,当我按原样运行您的代码时,我得到的结果是相同的。 "Done Creating..."消息在主线程进入2000ms睡眠之前打印,其中打印的时间戳是后台工作程序进入其工作方法的时刻。
Windows                      Mono
Done Creating...             Done Creating...
00:00:00.0018610             00:00:00.0088775
00:00:00.0019544             Start
Start                        00:00:00.5074494
Start                        Start
Done                         Done
Done                         00:00:00.5615083
00:00:00.5027995             Start
Start                        Done
00:00:00.5028008             00:00:01.0082133
Start                        Start
Done                         00:00:01.0082900
Done                         Start
00:00:01.0025428             Done
Start                        00:00:01.0618140
00:00:01.0026164             Start
Start                        Done
Done                         Done
Done                         Done
Really done                  Really done
我在两个完全相同的箱子上运行了测试(Dell T3400,相同的硬件),一个是Windows 7 x64,另一个是Ubuntu 12.04 x64。没有区别。确保你正在使用发布版本的可执行文件而不是附有任何调试器的版本进行测试。还要确保你在同一台硬件上进行测试。虽然可能性很小,但这可能是您的linux / mono安装的问题(假设您在linux上运行mono而不是windows)。正如我之前所说,并且我认为这些结果支持它,我认为这不是问题所在。我会在你的代码中寻找其他地方。

1
只是提出一些事情。 它不直接使用QueueUserWorkItem,而是使用BeginInvoke,最终使用线程池。此外,正在发生的是他达到了线程池的最低限制。一旦达到最低限制,它将在创建新线程之前等待一段时间。因此,在此实例中将最小值设置为6将防止发生延迟。 - Will
@High828 - 是的,那不是我想要表达的重点。当我说它调用QueueUserWorkItem时,我在脑海中考虑最简单的解释方式,我的意思是说它类似于用户调用该方法。个人而言,我倾向于遵循C# via CLR中的建议,尽量避免使用ThreadPool的线程操作方法。运行时通常比我更擅长确定线程性能。然而,我怀疑他的主应用程序没有达到ThreadPool的最小值,只是这个测试应用程序达到了。我认为他的问题在其他地方。 - Christopher Currens
从我的测试结果来看,似乎是这样的。对我来说,线程最小值为4,我一次可以同时运行4个批处理,然后再慢慢运行2个。当我将最小值提高到6时,它们都可以同时运行。此外,我同意您不应该编辑此线程池的最小值。我只是在解释发生了什么。当达到最小值时,它会立即停止创建线程。 - Will
感谢详细的回复。我没有意识到BackgroundWorker没有使用本地线程。然而,在C# .NET中,代码仍然完全正常工作;也就是说,它很快。在Mono上,它比“稍微慢一点”更慢,至少慢了10倍。在这种情况下,我将探索替代BackgroundWorker的方法,但由于上述原因,我怀疑代码不是确切的问题所在。 - Tom
此外,我不认为启动BackgroundWorkers会花费很长时间。当我说“长”时,我的意思是几秒钟的时间,这就是我看到的情况。这不是我在M$实现中看到的行为。 - Tom
很有趣。在我的情况下,我肯定正在使用相同的(相当新的)硬件,并且Mono确实在Linux上运行。我今天稍后会尝试发布版本,看看是否可以解决这个问题。 - Tom

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