【问题标题】:Multithread performance boost多线程性能提升
【发布时间】:2011-09-15 20:13:28
【问题描述】:

谁能告诉我为什么其中一种 DoCalculation 方法比另一种快得多(比如快 40%)?

我有等待设置 ManualResetEvents 的主线程:

private void LayoutRoot_Loaded(object sender, RoutedEventArgs e)
{
ThreadPool.QueueUserWorkItem((obj) =>
{
    ManualResetEvent[] finishcalc = new ManualResetEvent[] 
    { 
        new ManualResetEvent(false), 
        new ManualResetEvent(false), 
        new ManualResetEvent(false), 
        new ManualResetEvent(false), 
        new ManualResetEvent(false), 
        new ManualResetEvent(false) 
    };
    TimeSpan time1 = new TimeSpan(DateTime.Now.Ticks);
    DoCalculation(rand.Next(10), rand.Next(10), 1, finishcalc[0]);
    DoCalculation(rand.Next(10), rand.Next(10), 2, finishcalc[1]);
    DoCalculation(rand.Next(10), rand.Next(10), 3, finishcalc[2]);
    DoCalculation(rand.Next(10), rand.Next(10), 4, finishcalc[3]);
    DoCalculation(rand.Next(10), rand.Next(10), 5, finishcalc[4]);
    DoCalculation(rand.Next(10), rand.Next(10), 6, finishcalc[5]);

    if (WaitHandle.WaitAll(finishcalc))
    {            
        TimeSpan time2 =new TimeSpan(DateTime.Now.Ticks);
        AddTextAsync(string.Format("DoCalculation Finish in {0}\n" ,(time2-time1).TotalSeconds));
    }
});
}

然后我有一个方法,它创建另一个线程来按顺序进行一些计算,也就是说,我需要前一个线程的结果来继续下一个。我找到了两种方法,这是针对 Silverlight 的。

在第一个示例中,我正在创建一个新线程,它会等待每个连续计算完成后再继续:

void DoCalculation(int number1, int number2, int callid, ManualResetEvent calcdone)
{
    ThreadPool.QueueUserWorkItem((obj0) =>
    {
        AddTextAsync(string.Format("The values for Callid {0} are {1} and {2}\n", callid, number1, number2));
        int result = 0;
        ManualResetEvent mresetevent = new ManualResetEvent(false);
        ThreadPool.QueueUserWorkItem((obj) =>
        {
            result = number1 + number2;
            mresetevent.Set();
        });
        mresetevent.WaitOne();
        mresetevent.Reset();
        ThreadPool.QueueUserWorkItem((obj2) =>
        {
            result *= result;
            mresetevent.Set();
        });
        mresetevent.WaitOne();
        mresetevent.Reset();

        ThreadPool.QueueUserWorkItem((obj2) =>
        {
            result *= 2;
            mresetevent.Set();
        });
        mresetevent.WaitOne();
        AddTextAsync(string.Format("The result for Callid {0} is {1} \n", callid, result));
        calcdone.Set();
    });
}

DoCalculation 的第二个例子我使用一个类作为链接,将一个 Action 作为参数传递给 ThreadPool,并使用它作为回调来创建链中的第二个和第三个线程:

链接类:

public class CalcParams
{
    public int CallID;
    public ManualResetEvent ManualReset;
    public int Result;
    public Action<int, ManualResetEvent, int> CallbackDone;
}

异步服务示例::

public static void DownloadDataInBackground(CalcParams calcparams)
{
    WebClient client = new WebClient();
    Uri uri = new Uri("http://www.google.com");
    client.DownloadStringCompleted += (s, e) =>
    {
        CalcParams localparams = (CalcParams)e.UserState;
        localparams.CallbackDone(e.Result.Length + localparams.Result, localparams.ManualReset, localparams.CallID);
    };
    client.DownloadStringAsync(uri, calcparams);
}

以及改进后的doCalculation方法:

void DoCalculation(int number1, int number2, int callid, ManualResetEvent calcdone)
{
    ThreadPool.QueueUserWorkItem((obj0) =>
    {
        int result = number1+number2;
        doCalculationService.DownloadDataInBackground(new CalcParams()
        {
            Result = result,
            ManualReset = calcdone,
            CallID = callid,
            CallbackDone = (r, m, i) =>
            {
                int sqrt = r * r;
                doCalculationService.DownloadDataInBackground(new CalcParams()
                {
                    Result = sqrt,
                    CallID = i,
                    ManualReset = m,
                    CallbackDone = (r2, m2, i2) =>
                    {
                        int result2 = r2 * 2;
                        AddTextAsync(string.Format("The result for Callid {0} is {1} \n", i2, result2));
                        m2.Set();
                    }
                });
            }
        });
    });
}

谢谢。

【问题讨论】:

  • 你可以用秒表来测量时间,只是为了兴趣,结果是否相同?你也可以分享一下 AddTextAsync 的代码,也许有瓶颈?
  • 我相信秒表在 Silverlight 中不可用。我可以在其他平台上进行相同的测试,但现在我只对 silverlight 感兴趣。而且这两种方法之间的时间差异太大了,即使删除了额外的调用,我也不明白为什么......
  • 我认为这是示例代码,其中有很多 QueueUserWorkItem。事实上,家政费用远远超过你所做的工作量。
  • 您的新示例更加令人困惑。您正在调用ThreadPool.QueueUserWorkItem 来运行一个后台作业,该作业只是将 another 线程排队以在后台下载,然后链接到另一个类似的构造。总之,当你真的只需要一个线程时,你创建了 5 个线程,因为你的作业是按顺序执行的。您所拥有的与单个 QueueUserWorkItem 没有什么不同,其中包含对 DownloadString 的两次调用和一个按顺序执行的计算。
  • 发布问题的基本理念是提供最少量的代码来说明问题。不要将所有变体和试验都转嫁给读者。

标签: c# multithreading silverlight performance


【解决方案1】:

没有充分的理由致电ThreadPool.QueueUserWorkItem 然后立即等待它完成。也就是这样写:

ThreadPool.QueueUserWorkItem(() =>
    {
        // do stuff
        mevent.Set();
    });
mevent.WaitOne();

不会给你带来任何好处。您的主线程最终等待。其实还不如只写:

// do stuff

因为线程池必须启动一个线程。

您可以通过删除所有嵌套的“异步”工作来简化和加速您的第一个 DoCalculation 方法:

void DoCalculation(int number1, int number2, int callid, ManualResetEvent calcdone)
{
    ThreadPool.QueueUserWorkItem((obj0) =>
    {
        AddTextAsync(string.Format("The values for Callid {0} are {1} and {2}\n", callid, number1, number2));
        int result = 0;

        result = number1 + number2;
        result *= result;
        result *= 2;

        AddTextAsync(string.Format("The result for Callid {0} is {1} \n", callid, result));
        calcdone.Set();
    });
}

根据更新的问题进行编辑

您的新示例 #3 在某种程度上简化了事情,但仍然没有抓住重点。以下是您的新 DoCalculation 方法执行时发生的情况:

  1. ThreadQueue.QueueUserWorkItem 创建一个新线程并且DoCalculation 方法退出。您现在有一个后台线程正在运行。我们将此线程称为 1。
  2. 代码调用DownloadDataInBackground。该方法启动 另一个 线程以异步下载数据。调用该线程 2。
  3. 线程 1 退出。
  4. 当线程 2 完成下载时,它调用完成回调,该回调再次调用 DownloadDataInBackground。这会创建线程 3,开始执行,然后线程 2 退出。
  5. 当线程 3 完成下载时,它会调用完成回调,进行计算,输出一些数据,然后退出。

所以你启动了三个线程。从来没有任何有意义的“多线程”在进行。也就是说,从来没有超过一个线程在做有意义的工作。

您的任务是按顺序执行的,因此没有理由启动多个线程让它们运行

如果您刚刚编写代码,您的代码会更简洁,执行速度也会更快(因为不必启动这么多线程):

ThreadPool.QueueUserWorkItem((obj0) =>
{
    DownloadString(...); // NOT DownloadStringAsync
    DownloadString(...);
    // Do calculation
});

一个线程按顺序执行每个任务。

您需要多个线程的唯一情况是您希望多个任务同时执行。显然,这不是你在做什么。实际上,您的问题是:

然后我有一个方法,它创建另一个线程来顺序执行一些计算,也就是说,我需要前一个线程的结果来继续下一个。

顺序任务意味着一个线程。

【讨论】:

  • 感谢您的回答,但也许我不够清楚,这是一个示例,其中每个计算都代表对 Web 服务的异步调用,或者我们不想将其作为主要任务的耗时任务线程在继续其他线程之前等待它。
  • 在我的情况下,我使用的是通过套接字与服务器进行网络通信的 OOB 应用程序,我使用的套接字客户端接受 Action 作为参数,因此我可以设置调用线程从回调方法,我还传递了其他参数,如调用者 ID、服务器响应等,所以我可以使用这些值将它们作为参数传递给下面的线程。这里的整个想法是性能如何根据你的链接方式发生变化您调用异步服务。
  • 我想我还是不明白。如果您的每个异步调用都依赖于先前调用返回的值(即您正在链接,如您的示例中所示),那么您可以将整个事情包装到一个异步块中,如我所示。您的两个示例均未显示执行并发异步任务的 DoCalculation 方法,因此不需要您建议的复杂链接。如果您的真实应用程序的 DoCalculation 方法确实执行并发异步任务,您应该更改示例以显示这一点。
  • 我更新了第二个例子来展示如何从链中调用异步服务,希望这更有意义。
  • 再次感谢您的回答@JimMischel,我实际上已经根据您的第一个建议更新了我的生产应用程序中的代码,并且它运行良好......,Silverlight 应用程序不能简单地同步正如你所建议的。除非您添加对其他框架的引用,例如 CaliburnMicro,否则我已经读过 Caliburn 包含非常好的东西,例如协程,这将使我们能够完全按照您的建议进行操作,我将尝试使用这些协程准备另一个示例并将它们作为基准好吧。
【解决方案2】:

我能否建议您将响应式扩展 (Rx) 作为在 Silverlight 中使用多线程的替代方法?

这是您在 Rx 中完成的代码:

Func<int, int, int> calculation = (n1, n2) =>
{
    var r = n1 + n2;
    r *= r;
    r *= 2;
    return r;
};

var query =
    from callid in Observable.Range(0, 6, Scheduler.ThreadPool)
    let n1 = rand.Next(10)
    let n2 = rand.Next(10)
    from result in Observable.Start(() => calculation(n1, n2))
    select new { callid, n1, n2, result };

query.Subscribe(x => { /* do something with result */ });

它自动将计算推送到线程池中 - 我将 Scheduler.ThreadPool 参数放入其中,但它是 SelectMany 查询的默认值。

使用这种代码,您通常不必担心所有的 MRE,并且可以轻松阅读可以更轻松地测试的代码。

Rx 是受支持的 Microsoft 产品,可在桌面 CLR 和 Silverlight 上运行。

这里是 Rx 的链接:

哦,我认为你得到非常不同的性能结果的原因是 Silverlight 只有毫秒级的时间分辨率,所以你真的需要运行数千次计算才能得到一个好的平均值。


编辑:根据 cmets 中的请求,这里是使用 Rx 链接每个中间计算的结果的示例。

Func<int, int, int> fn1 = (n1, n2) => n1 + n2;
Func<int, int> fn2 = n => n * n;
Func<int, int> fn3 = n => 2 * n;

var query =
    from callid in Observable.Range(0, 6, Scheduler.ThreadPool)
    let n1 = rand.Next(10)
    let n2 = rand.Next(10)
    from r1 in Observable.Start(() => fn1(n1, n2))
    from r2 in Observable.Start(() => fn2(r1))
    from r3 in Observable.Start(() => fn3(r2))
    select new { callid, n1, n2, r1, r2, r3 };

当然,这三个 lambda 函数可以很容易地改为常规方法函数。

如果您有使用BeginInvoke/EndInvoke 异步模式的函数,另一种选择是使用FromAsyncPattern 扩展方法,如下所示:

Func<int, int, IObservable<int>> ofn1 =
    Observable.FromAsyncPattern<int, int, int>
        (fn1.BeginInvoke, fn1.EndInvoke);

Func<int, IObservable<int>> ofn2 =
    Observable.FromAsyncPattern<int, int>
        (fn2.BeginInvoke, fn2.EndInvoke);

Func<int, IObservable<int>> ofn3 =
    Observable.FromAsyncPattern<int, int>
        (fn3.BeginInvoke, fn3.EndInvoke);

var query =
    from callid in Observable.Range(0, 6, Scheduler.ThreadPool)
    let n1 = rand.Next(10)
    let n2 = rand.Next(10)
    from r1 in ofn1(n1, n2)
    from r2 in ofn2(r1)
    from r3 in ofn3(r2)
    select new { callid, n1, n2, r1, r2, r3 };

前面有点混乱,但查询要简单一些。

注意:Scheduler.ThreadPool 参数再次是不必要的,只是为了明确显示查询使用线程池执行。

【讨论】:

  • 我在 3 个月前开始我正在工作的项目时正在考虑使用 RX,当时我不知道 RX 可用于 silverlight,在再次询问之前,我试图找到出了如何链接每个线程的3个操作,我找不到任何例子,你能扩展你的吗?假设这 3 个操作中的每一个都是异步执行的,并且您需要前一个操作的结果来执行下一个操作。谢谢。
  • @montelof - 很容易将操作与 Rx 链接在一起。我将根据要求使用示例编辑我的解决方案。 :-)
猜你喜欢
  • 2018-06-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多