【问题标题】:C# Threading, can multiple threads calling same method accessing the same array cause slowness?C# Threading,多个线程调用同一个方法访问同一个数组会导致缓慢吗?
【发布时间】:2012-06-20 21:59:15
【问题描述】:

背景: 我希望我的问题不是太含糊。我会尽量解释,不能贴太多代码,因为受影响的方法非常复杂冗长。所以我的问题是我试图加快多线程的处理速度。该应用程序用于在我的 2D 游戏引擎中进行照明,我在场景顶部绘制不同透明度级别的黑色矩形(目前导致延迟)。

对我来说,第一步是将具有相同亮度的相邻矩形批处理在一起,以减少一些渲染工作。这效果很好(也许可以做得更好,但现在不是重点),现在我还实现了一个粗略的线程系统,它将每批一起为屏幕的一个单独部分照明。

我的问题的关键:在分析上述特定批处理方法的时间时(针对不同数量的线程处理单独的、不相交的数据集),我注意到一些奇怪的峰值。以前,当使用一个线程时,该方法执行大约需要 12ms,奇数跳转到 15ms。我没想太多。然而,当使用 2 或 3 个线程进行处理时,我得到或多或少的 4~5 毫秒,跳跃到 10 毫秒,有时甚至高达 20 毫秒。

现在我意识到,如果不检查我的代码,任何人都无法说出原因可能是什么,所以我不希望这样。相反,我试图得出一些结论,并希望现在确认它们。如前所述,每个线程都在我的数据集的一部分上工作,彼此完全不相交(它们不重叠)。但是,数据数组的入口点是通过特定类的相同实例的相同方法。所以线程都通过相同的方法访问同一个数组(但它的不同部分 - 因此我也不使用任何锁)。这会导致意外减速吗?

或者线程以这种方式操作可能是正常行为(执行时间变化到标准的两倍以上)?另外,我在需要的时候创建所有线程他们,让他们运行到完成。

数据集的访问方式:

public short GetLightLevelAt(int x, int y)
{
   if (inLightingBounds(x, y))
   {
      return lightData[x, y];
   }
   else
   {
      return GuessLightLevelAt(x,y); //This won't ever happen currently, guaranteed
   }
}

【问题讨论】:

  • 您的计时测量结果表明:1. 发生阻塞时线程之间发生了一些争用,和/或 2. 操作系统正在进行一些后台处理。
  • 由于底层硬件特性,访问数组肯定有好方法和坏方法。 GC 是否有可能导致您的峰值?是否有正在创建/销毁的对象?
  • @RobertHarvey 我怀疑争用,但我的知识可能有点有限,无法确定为什么会出现这种情况。您能否详细说明争用是如何发生的?这或多或少是我期望的答案!
  • 当您lock 某物,而其他线程试图访问它时,就会发生争用。第二个线程必须等待第一个线程释放它的锁。
  • @itsme86 嗯,这很可能是一个原因。我什至没有想到这一点。我的数据集最初非常小,我创建了一些数组来帮助我进行批处理。它们曾经很小,因为照明分辨率曾经很可怕(大矩形)。但现在我打算将矩形缩小到一个像素的大小。所以数据结构要大得多。我现在会调查的!谢谢!

标签: c# multithreading performance


【解决方案1】:

在您的情况下,它可能与上面提到的垃圾收集或上下文切换有关,因为峰值很高。

但是,在同一阵列上运行多线程可能仍会由于错误共享而出现速度下降。 (即使分区不重叠)

例如,以下代码可能存在错误共享问题:

int[] array = new int[100000];
int a = 1;
int b = 0;

//The following loops run concurrently

//Thread A
for( int i = 0; i < 50000; i++ )
    array[i] = a;

//Thread B
for( int j = 50000; j < 100000; j++ )
    array[j] = b;

发生错误共享是因为 int a 和 b 被声明为彼此相邻,并且很可能彼此位于同一缓存行上。

如果同时运行,此代码可能根本不会加速,甚至可能会减速。

【讨论】:

  • 我认为这与我所拥有的非常匹配。有没有办法克服这个问题?也许只是按顺序而不是同时进行?或者我可以以某种方式重组我的数据吗?可能是分开了还是怎么的?您将如何解决示例中提出的问题?
  • 如果您在 a 和 b 之间声明其他值,则可以强制它们位于不同的缓存行上。在这个例子中,我可以将“int a”的声明放在数组之前,并将“b”的声明放在后面。或者只是在“a”和“b”之间声明一个 int[] padding = new int[33](也可以尝试与 33 不同的大小)。有多种方法可以以编程方式确定缓存行的大小,但反复试验可能是最简单的。
【解决方案2】:

也许您的数据集与垃圾 CPU 缓存一样大,因此性能不一致?由于有多个级别的缓存并且无法控制它,您永远无法准确判断某项内容需要多少。

例如,如果您的数据与处理器上最私密的缓存大小一样大,并且您从一个线程转移到三个线程,那么您将有效地放弃它本来可以为您带来的任何性能优势。有时,一个接一个地依次执行会更好。

【讨论】:

  • 这也是我的想法之一(虽然不是很自信)。感谢您的洞察力。我想这将是一个难以测试的测试
  • 只有当你找到如何关闭缓存并重新运行整个事情时。
  • 哦,聪明 :) 也许我也应该调查一下。
  • 我尝试在添加线程之前按顺序执行此操作。这只是为了帮助我拆分数据处理等。并没有真正影响性能。如果我把它分成更多,也许它可以。我只把它分成 2 份。也许如果我把它分成更多,它会适合缓存。虽然,我的方法很长,而且还有一些很长的嵌套循环。在我看来,缓存不太友好。
  • 在 1MB 数据上运行的长嵌套循环是缓存的设计目的......您的数据大小是多少?
【解决方案3】:

您遇到的开销可能来自上下文切换或资源争用。一旦你最大化可用内核,操作系统将开始上下文切换(基本上从一个进程/线程移动到另一个进程/线程)。

不过不贴代码就很难分辨。

【讨论】:

  • 我意识到这一点。我正在努力寻找一段足够小但可以发布的代码。我目前在我的四核上只使用 3 个工作线程。所以它不应该是上下文切换,对吧?
  • @Denzil :你必须记住,你的程序并不是机器上唯一运行的东西。
  • 是的,这是真的。我想我可以假设后台程序的影响可以忽略不计。
【解决方案4】:

您询问从少数线程切换到多线程来运行该方法是否会导致延迟。简短的回答是

您没有锁定该数组,因此与争用无关。

GC 会随机出现在一段代码中,所以你会在这里和那里看到尖峰,而不是在特定的地方。

我敢打赌这不是一个尖峰。这是上下文切换。您的时钟分辨率很可能很高,并且您的量子尺寸也很高。所以一个单一的上下文切换会长时间停止你的线程。

如果这是您遇到麻烦的原因,那么您需要减少线程数。你让它比它可能的慢。

【讨论】:

    猜你喜欢
    • 2017-06-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-08-01
    相关资源
    最近更新 更多