2011-09-15 112 views
1

有人能告訴我爲什麼其中一個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的第二個例子傳遞一個作爲行動參數提供給線程池,並用它作爲回調在鏈來創建第二和第三螺紋:

鏈接類:一個ASY的

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

示例NC服務::

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(); 
        } 
       }); 
      } 
     }); 
    }); 
} 

謝謝。

+0

您是否可以使用StopWatch來衡量時間,僅僅爲了興趣,結果是否相同?也可以分享請AddTextAsync的代碼也許有瓶頸? – sll

+0

我相信秒錶在Silverlight中不可用..我可以在其他平臺上測試它,但現在我只對silverlight感興趣。而且這兩種方法之間的時間差異太大,甚至刪除額外的呼叫,我不明白爲什麼...... – montelof

+0

我認爲這是示例代碼,裏面有很多QueueUserWorkItem。事實上,家務成本遠遠超過你所做的工作量。 – Skizz

回答

2

我可以建議你看看Reactive Extensions(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上運行。

這裏是鏈接,接收:

哦,我覺得你得到非常不同的性能結果的原因是,Silverlight的只有毫秒時間分辨率,所以你將不得不運行計算數千次才能獲得良好的平均值。


EDIT:按照評價的請求,這裏是使用鏈接的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參數同樣是不必要的,只是包含在內以明確顯示查詢使用線程池執行。

+0

我正在考慮使用RX,當我開始我在3個月前工作的項目時,我不知道RX在那個時候可用於silverlight,再次問你之前,我試圖找出如何鏈接3個操作每一個線程,我找不到任何例子,你能擴展你的嗎?想象3個操作中的每一個都是以異步方式執行的,並且您需要前一個操作的結果才能執行下一個操作。謝謝。 – montelof

+0

@montelof - 將操作與Rx連鎖在一起很容易。我將按照要求使用示例編輯我的解決方案。 :-) – Enigmativity

3

沒有很好的理由叫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 
}); 

一個線程按順序執行每個任務。

您需要多線程的唯一時間是如果您想要同時執行多個任務。顯然,這不是你在做什麼。事實上,你的問題說:

然後我有一個方法,創建另一個線程進行一些計算順序,這是,我需要從前一個線程的結果繼續下面的一個。

順序任務意味着一個線程。

+0

謝謝你的回答,但也許我不夠清楚,這是一個例子,其中每個計算代表一個異步調用Web服務,或一個耗時的任務,我們不想要使主線程在繼續執行其他線程之前等待它。 – montelof

+0

在我的情況下,即時通訊使用一個OOB應用程序,該應用程序通過套接字與服務器進行網絡通信,使用的socketclient接受一個Action 作爲參數,所以我可以從回調方法設置調用線程,傳遞其他參數,例如調用者ID,服務器響應等,以便我可以使用這些值將它們作爲參數傳遞給以下線程。這裏的整個想法是如何根據將調用鏈接到異步服務的方式來改變性能。 – montelof

+0

我想我還是不明白。如果你的每個異步調用都依賴於前面調用返回的值(即你正在鏈接,就像在你的例子中那樣),那麼你可以把整個東西包裝到一個單獨的異步塊中,如我所示。您的示例都沒有顯示執行併發異步任務的「DoCalculation」方法,因此不需要您提出的複雜鏈接。如果您真正的應用程序的DoCalculation方法*執行併發異步任務,則應該更改示例以顯示該示例。 –