【问题标题】:Thread usage for optimization用于优化的线程使用
【发布时间】:2010-12-16 21:00:54
【问题描述】:

这是一段 C# 代码,它对双精度矩阵的每一行(假设为 200x200)应用一个操作。

For (int i = 0; i < 200; i++)
{
   result = process(row[i]);
   DoSomething(result);
}

进程是一个静态方法,我有一个Corei5 CPU和Windows XP em>,我正在使用 .Net Framework 3.5。为了获得性能,我尝试使用单独的线程(使用异步委托)处理每一行。于是我将代码改写如下:

List<Func<double[], double>> myMethodList = new List<Func<double[], double>>();
List<IAsyncResult> myCookieList = new List<IAsyncResult>();
for (int i = 0; i < 200; i++)
{
   Func<double[], double> myMethod = process;
   IAsyncResult myCookie = myMethod.BeginInvoke(row[i], null, null);
   myMethodList.Add(myMethod);
   myCookieList.Add(myCookie);
}
for (int j = 0; j < 200; j++)
{
   result = myMethodList[j].EndInvoke(myCookieList[j]);
   DoSomething(result);
}

此代码在一次运行中被调用 1000 个矩阵。当我测试时,令人惊讶的是我没有得到任何性能提升!所以这给我带来了这个问题,在什么情况下多线程将有利于性能提升,而且我的代码是否合乎逻辑?

【问题讨论】:

  • 它是哪个 Core i5?可用线程的数量受到限制,具体取决于它是哪个芯片。此外,我认为您不会看到大幅增长,但我很惊讶您说您没有任何改善。
  • 当执行时间超过一秒时,您应该能够使用 TaskManager 来判断是否所有内核都在使用。

标签: c# multithreading performance asynchronous delegates


【解决方案1】:

乍一看,您的代码看起来不错。也许CPU不是瓶颈。

您能否确认process()DoSomething() 是独立的并且不对共享资源进行任何I/O 或锁定?

这里的重点是您必须开始测量。

当然,带有 TPL 的 Fx4 使这种东西更容易编写并且通常更高效。

【讨论】:

  • +1 用于 TPL,这可以在 .Net 3.5 中使用(它是 Reactive Extensions 的一部分)。
【解决方案2】:

您可以通过使用AsyncCallback 调用BeginInvoke 来实现更多的并行性(特别是在结果处理中)——这将在ThreadPool 线程中进行结果处理,而不是像您目前拥有的那样内联。

请参阅异步编程文档here 的最后一部分。

在您对代码进行任何修改之前,您应该对其进行分析以找出程序将时间花在哪里。

【讨论】:

  • 你确定吗?你有那个链接吗?
  • @Henk - 除了我在答案中包含的那个之外,没有。请参阅Executing a Callback Method When an Asynchronous Call Completes 部分
  • 啊,我明白了。我认为该页面声明回调将在线程池以及上执行,而不是没有回调的执行是内联的。我认为这没有意义。
  • @Henk - 是的,我的意思是 DoSomething 将在线程池线程上的回调函数中执行。
  • @Steve,同意,OP 可能应该将 2 种方法合并为 1 种。如果它们独立于机器人等。
【解决方案3】:

您的代码有点过火了。看看循环;对于 200 次迭代中的每一次,您都在创建一个新线程来进行异步调用。这将导致您的进程有 201 个活动线程。有一个收益递减规律;线程数大约是处理器拥有的“执行单元”数量的两倍(CPU 数量,乘以每个 CPU 上的内核数量,如果内核可以超线程,则为 X2),您的计算机将开始花费调度线程的时间多于运行它们所花费的时间。最先进的服务器具有 4 个四核 HT CPU,用于大约 32 个 EU。 200 个积极执行的线程会让这个服务器崩溃和哭泣。

如果处理顺序无关紧要,我会实现一个类似 MergeSort 的算法;将数组分成两半,处理左手,处理右手。每个“左手”都可以由一个新线程处理,但在当前线程中处理“右手”。然后,实现一些线程安全的手段,将线程数限制在“执行单元”数量的 1.25 倍左右;如果已达到限制,则继续线性处理而不创建新线程。

【讨论】:

  • 他不是在创建 200 个线程,而是在 ThreadPool 上排队 200 个作业。 TP 将(缓慢地)创建额外的线程来处理它们。
  • ...但最后我检查了一下,ThreadPool 并没有“智能地”限制其作业的线程数;您必须手动将其限制为适合您环境的合理数字,否则如果线程足够长,它确实会创建 200 个工作线程。
  • 不,它不明智地限制了它。它通过最多创建 1 个线程/500 毫秒来阻止。不过,Fx4 TP(有点)更智能。可惜 OP 没有发布 abs 运行时间。
【解决方案4】:

由于您处理 EndInvoke 方法调用的方式,您似乎没有获得任何性能。由于您正在使用 BeginInvoke 调用“进程”,因此这些函数调用会立即返回,因此第一个循环可能会立即完成。但是,EndInvoke 会一直阻塞,直到正在调用它的调用完成处理,您仍在按顺序使用它。正如史蒂夫所说,您应该使用 AsyncCallback 以便每个完成事件都在它自己的线程上处理。

【讨论】:

  • 我认为 EndInvoke() 没有任何问题。因为当我们等待第一个线程完成时,其他线程也在处理,当第一个结果出来时,其他活动线程也产生了它们的结果。所以循环的下一次迭代不会等待剩余的线程,因为它们的结果已经在第一次迭代中生成了。
【解决方案5】:

您没有看到太多收益,因为您没有并行化代码,是的,您正在执行异步,但这只是意味着您的循环不会等待计算进入下一步。使用 Parallel.For 而不是 for 循环,看看你的多核盒子是否有任何收获......

【讨论】:

    【解决方案6】:

    如果您要使用异步委托,这将是确保回调发生在线程池线程上的方法;

            internal static void Do()
        {
            AsyncCallback cb = Complete;
    
            List<double[]> row = CreateList();
            for (int i = 0; i < 200; i++)
            {
                Func<double[], double> myMethod = Process;
                myMethod.BeginInvoke(row[i], cb, null);
            }
        }
        static double Process (double[] vals)
        {
           // your implementation
            return randy.NextDouble();
        }
        static void Complete(IAsyncResult token)
        {
    
            Func<double[], double> callBack = (Func<double[], double>)((AsyncResult)token).AsyncDelegate;
            double res = callBack.EndInvoke(token);
    
            Console.WriteLine("complete res {0}", res);
            DoSomething(res);
    
    
        }
    

    【讨论】:

      猜你喜欢
      • 2012-05-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-05-03
      • 2016-03-07
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多