2012-01-27 76 views
3

以下代碼是我試圖優化的代碼的簡化版本。在IEnumerable實現中使用任務並行庫來實現速度改進

void Main() 
{ 
    var words = new List<string> {"abcd", "wxyz", "1234"}; 

    foreach (var character in SplitItOut(words)) 
    { 
     Console.WriteLine (character); 
    } 
} 

public IEnumerable<char> SplitItOut(IEnumerable<string> words) 
{ 
    foreach (string word in words) 
    { 
     var characters = GetCharacters(word); 

     foreach (char c in characters) 
     { 
      yield return c; 
     } 
    } 
} 

char[] GetCharacters(string word) 
{ 
    Thread.Sleep(5000); 
    return word.ToCharArray(); 
} 

我無法更改方法SplitItOut的簽名。GetCharacters方法調用代價昂貴,但是線程安全。 SplitItOut方法的輸入可以包含100,000個以上的條目,並且對GetCharacters()方法的單個調用可能需要大約200ms。它也可以拋出我可以忽略的異常。結果順序無關緊要。

在我的第一次嘗試中,我提出了使用TPL進行後續實現,它加速了相當多的事情,但在我處理完所有單詞之前一直阻塞。

public IEnumerable<char> SplitItOut(IEnumerable<string> words) 
{ 
    Task<char[][]> tasks = Task<char[][]>.Factory.StartNew(() => 
    { 
     ConcurrentBag<char[]> taskResults = new ConcurrentBag<char[]>(); 

     Parallel.ForEach(words, 
      word => 
      { 
       taskResults.Add(GetCharacters(word)); 
      }); 

     return taskResults.ToArray(); 
    }); 

    foreach (var wordResult in tasks.Result) 
    { 
     foreach (var c in wordResult) 
     { 
      yield return c; 
     } 
    } 
} 

我正在尋找方法SplitItOut()比這更好的實現。較低的處理時間是我的首要任務。

+0

輸出爲什麼你有一個'Thread.sleep代碼(5000)在' 'GetCharacters'?我敢打賭,這就是爲什麼你的算法很慢。 – 2012-01-27 02:43:29

+0

這只是一個示例代碼。 Thread.Sleep(5000)只是爲了表明實際的GetCharacters()方法調用起來很昂貴。 – Snakebyte 2012-01-27 02:47:02

+0

什麼是它的約束?中央處理器?磁盤?網絡? – 2012-01-27 02:47:50

回答

4

如果我正確地讀你的問題,你不希望只是加快創建從字字符的並行處理 - 您希望您的枚舉在準備好後立即生成每個。通過您目前的實施(以及我目前看到的其他答案),SplitItOut將等到所有單詞已發送至GetCharacters,並且在生成第一個單詞之前返回所有結果。

在這種情況下,我喜歡把事情分解爲生產者和消費者。您的製作人線程將採用可用的單詞並調用GetCharacters,然後將結果轉儲到某處。 消費者一旦準備好就會向SplitItOut的呼叫者發送字符。真的,消費者是SplitItOut的來電者。

我們可以利用BlockingCollection作爲產生字符的方式,並作爲放置結果的「某處」。我們可以使用ConcurrentBag作爲一個地方把那些尚未被分割的話:

static void Main() 
     { 
      var words = new List<string> { "abcd", "wxyz", "1234"}; 

      foreach (var character in SplitItOut(words)) 
      { 
       Console.WriteLine(character); 
      } 
     } 


     static char[] GetCharacters(string word) 
     { 
      Thread.Sleep(5000); 
      return word.ToCharArray(); 
     } 

沒有改變你的mainGetCharacters - 因爲這些代表你的限制(不能更改來電者,不能改變昂貴的操作)

 public static IEnumerable<char> SplitItOut(IEnumerable<string> words) 
     { 
      var source = new ConcurrentBag<string>(words); 
      var chars = new BlockingCollection<char>(); 

      var tasks = new[] 
        { 
         Task.Factory.StartNew(() => CharProducer(source, chars)), 
         Task.Factory.StartNew(() => CharProducer(source, chars)), 
         //add more, tweak away, or use a factory to create tasks. 
         //measure before you simply add more! 
        }; 

      Task.Factory.ContinueWhenAll(tasks, t => chars.CompleteAdding()); 

      return chars.GetConsumingEnumerable(); 
     } 

在這裏,我們改變了SplitItOut方法做四件事情:

  1. 初始化concurre ntbag包含我們希望分割的所有詞語。 (注意:如果你想按需要枚舉字,你可以開始一個新任務來推送它們,而不是在構造函數中執行它)
  2. 啓動我們的字符「生產者」任務。你可以啓動一個號碼,使用工廠,不管。在測量之前,我建議不要發瘋。
  3. 指示我們在所有任務完成後完成的BlockingCollection
  4. 「消費」所有的生產字符的(我們可以很容易在自己身上,就回到一個IEnumerable<char>而非的foreach和產量,但你可以做到這一點很長的路要走,如果你願意的話)

唯一缺少是我們的生產者實施。我已經擴展了所有的LINQ的快捷方式,使之清楚,但它是超級簡單:

 private static void CharProducer(ConcurrentBag<string> words, BlockingCollection<char> output) 
     { 
      while(!words.IsEmpty) 
      { 
       string word; 
       if(words.TryTake(out word)) 
       { 
        foreach (var c in GetCharacters(word)) 
        { 
         output.Add(c); 
        } 
       } 
      } 
     } 

這只是

  1. 注意到一個字的ConcurrentBag的(除非它是空的 - 如果是,任務完成!)
  2. 調用昂貴的方法
  3. 提出在BlockingCollection
+0

看起來很有希望。讓我試着用我原來的算法實現這個模式,看看它是如何工作的。謝謝。 – Snakebyte 2012-01-27 19:57:43

+0

+1但這裏TPL的問題在哪裏問過? – 2013-03-18 07:45:30

+0

@ GennadyVanin - 新西伯利亞:System.Threading.Tasks.Task構成了TPL的基礎,所以我不確定你的困惑在哪裏。請參見上面SplitItOut創建生產者任務然後等待結果。 – 2013-03-18 15:43:22

2

我把你的代碼通過內置到Visual Studio中的剖析器,它看起來像任務的開銷正在傷害你。我稍微重構它以刪除Task,並且它稍微提高了性能。如果沒有您的實際算法和數據集,很難確切地說出問題所在或者性能可以提高的地方。如果你有VS Premium或Ultimate,那麼內置的分析工具可以幫助你解決很多問題。您也可以抓取ANTS的試用版。

有一點要記住:不要試圖過早優化。如果您的代碼的性能可以接受,請不要添加任何內容到可能使代碼更快,但代價是可讀性和可維護性。如果它沒有達到可接受的水平,請在開始搞亂之前對其進行分析。

在任何情況下,這是我的算法的重構:

public static IEnumerable<char> SplitItOut(IEnumerable<string> words) 
    { 
     var taskResults = new ConcurrentBag<char[]>(); 

     Parallel.ForEach(words, word => taskResults.Add(GetCharacters(word))); 

     return taskResults.SelectMany(wordResult => wordResult); 
    }