2

對不起,大量的代碼,我不能解釋與less.Basically我試圖從許多任務寫入文件。 你們能告訴我我做錯了什麼嗎? _streamWriter.WriteLine()會拋出ArgumentOutOfRangeExceptionStreamwriter,StringBuilder和並行循環

class Program 
{ 
    private static LogBuilder _log = new LogBuilder(); 
    static void Main(string[] args) 
    { 
     var acts = new List<Func<string>>(); 
     var rnd = new Random(); 
     for (int i = 0; i < 10000; i++) 
     { 
      acts.Add(() => 
      { 
       var delay = rnd.Next(300); 
       Thread.Sleep(delay); 
       return "act that that lasted "+delay; 
      }); 
     } 

     Parallel.ForEach(acts, act => 
     { 
      _log.Log.AppendLine(act.Invoke()); 
      _log.Write(); 
     }); 
    } 
} 

public class LogBuilder : IDisposable 
{ 
    public StringBuilder Log = new StringBuilder(); 
    private FileStream _fileStream; 
    private StreamWriter _streamWriter; 

    public LogBuilder() 
    { 
     _fileStream = new FileStream("log.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); 
     _streamWriter = new StreamWriter(_fileStream) { AutoFlush = true }; 
    } 
    public void Write() 
    { 
     lock (Log) 
     { 
      if (Log.Length <= 0) return; 
      _streamWriter.WriteLine(Log.ToString()); //throws here. Although Log.Length is greater than zero 
      Log.Clear(); 
     } 
    } 

    public void Dispose() 
    { 
     _streamWriter.Close(); _streamWriter.Dispose(); _fileStream.Close(); fileStream.Dispose(); 
    } 
} 
+0

你有一個堆棧跟蹤?我沒有看到'Streamwriter.WriteLine(object)'拋出異常http://msdn.microsoft.com/en-us/library/4zcc928k.aspx。 (奇怪的是MSDN在來自StreamWriter時指向TextWriter) – Prescott 2012-02-17 16:19:28

+0

哦..也許這是StringBuilder的責任? http://msdn.microsoft.com/en-us/library/system.text.stringbuilder(v=VS.100).aspx – Agzam 2012-02-17 16:34:09

+0

嗯..它似乎是這樣...我用「for」替換Log.ToString() 「循環,它的工作.. – Agzam 2012-02-17 16:36:18

回答

4

這不是StringBuilder中的錯誤,它是代碼中的錯誤。並且在後續答案中顯示的修改(如果將Log.String替換爲一次提取一個字符的循環)不能解決此問題。它不會再拋出異常,但它也不會正常工作。

的問題是,你使用的兩種StringBuilder地方你的多線程代碼,其中一個不試圖將其鎖定,這意味着可以在一個線程中同時出現的另一個寫作發生讀數。特別是,這個問題是這條線:

_log.Log.AppendLine(act.Invoke()); 

你在Parallel.ForEach內這樣做。你不會在這裏嘗試任何同步,儘管這會一次在多個線程上運行。所以,你有兩個問題:

  1. 多次調用AppendLine可以同時進行多線程
  2. 一個線程可能試圖在同一時間爲一個或多個其他線程調用將被Log.ToString調用AppendLine

由於您使用lock關鍵字來同步這些關鍵字,所以一次只能讀取一個。問題是,撥打AppendLine時,您還沒有獲得相同的鎖定。

你的'修復'並不是一個真正的修復。你只能使問題更難以看清。它現在只會以不同的和更微妙的方式出錯。例如,我假設您的for循環完成其最終迭代後,您的Write方法仍會繼續調用Log.Clear。那麼在完成最後一次迭代和撥打電話Log.Clear之間,有可能其他線程會再次撥打電話AppendLine,因爲這些電話與AppendLine之間沒有同步。

結果是,你有時會錯過一些東西。代碼將寫入字符串生成器中的東西,然後將其清除,而不會寫入流寫入器。

此外,併發AppendLine調用造成問題的可能性很大。如果你幸運的話,他們會不時崩潰。 (這是一件好事,因爲它明確,你必須解決的一個問題。)如果你運氣不好,你只是得到的數據損壞,不時 - 兩個線程可能最終會寫入在StringBuilder同一個地方導致無論是在一團糟,或完全丟失的數據。

再次,這是不是在StringBuilder的錯誤。它並不旨在支持從多個線程同時使用。這是你的工作,以確保只有一次一個線程做什麼的StringBuilder任何特定實例。正如該類的文檔所述,「任何實例成員都不能保證線程安全。」

顯然,你不希望在你打電話act.Invoke(持有該鎖),因爲這大概是很努力要並行。所以我猜這樣的事情可能會工作得更好:

string result = act(); 
lock(_log.Log) 
{ 
    _log.Log.AppendLine(result); 
} 

但是,如果我離開那裏,我不會真的是幫你,因爲這看起來非常錯誤的我。

如果你發現自己鎖在別人的對象的字段,它在你的代碼中的設計問題的徵兆。修改設計可能更有意義,因此LogBuilder.Write方法接受一個字符串。說實話,我甚至不知道你爲什麼在這裏使用StringBuilder,因爲你似乎只是把它當作一個字符串的保存區域,而你立即寫入流寫入器。你希望StringBuilder會在這裏添加什麼?以下是簡單,似乎並沒有失去任何東西(比原來的併發錯誤等):

public class LogBuilder : IDisposable 
{ 
    private readonly object _lock = new object(); 
    private FileStream _fileStream; 
    private StreamWriter _streamWriter; 

    public LogBuilder() 
    { 
     _fileStream = new FileStream("log.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); 
     _streamWriter = new StreamWriter(_fileStream) { AutoFlush = true }; 
    } 
    public void Write(string logLine) 
    { 
     lock (_lock) 
     { 
      _streamWriter.WriteLine(logLine); 
     } 
    } 

    public void Dispose() 
    { 
     _streamWriter.Dispose(); fileStream.Dispose(); 
    } 
}  
+0

最初的想法是使用StringBuilder來收集logevents並僅當它獲得超過50行時輸出到日誌文件中。 – Agzam 2012-02-21 15:33:01

+0

好的,有道理。在這種情況下,我會讓StringBuilder成爲LogBuilder的私有成員,從而消除了在課堂外使用它的誘惑 - 這是造成問題的原因。然後,可以很簡單地確保你每次追加到StringBuilder時都會保持一定的鎖定,這將消除你所看到的問題,並使你能夠實現這種緩衝策略。 (儘管你也應該寫一些測試來驗證這些批量寫作是否能帶來你希望的任何好處。) – 2012-02-23 15:52:26

1

我認爲原因是因爲你在中並行支架訪問StringBuilder的

_log.Log.AppendLine(act.Invoke()); 
_log.Write(); 

和LogBuilder內執行鎖()將禁止在stringBuidler內存分配。你正在改變streamwriter來處理每個字符的日誌,因此會讓parellel進程解鎖stringBuilder的內存分配。

·隔離並行處理成不同的行動通過TextWriter.Synchronized到可能會減少的問題

Parallel.ForEach(acts, act => 
{ 
    _log.Write(act.Invoke()); 
}); 

在LogBuilder類

private readonly object _lock = new object(); 

public void Write(string logLines) 
{ 
    lock (_lock) 
    { 
     //_wr.WriteLine(logLines); 
     Console.WriteLine(logLines); 
    } 
} 
0

的代碼可以更簡單比@IanGriffiths回答在StreamWriter周圍創建一個鎖定包裝。該框架在許多類上提供Synchronized helpers以在需要時創建線程安全實例。

我正在打印當前循環迭代變量i,因此可以很容易地看到並行性在調度中的影響。當作業開始時,必須使用局部變量來存儲i的當前值。此技術將避免捕獲閉包中的迭代變量的引用,每次打印變量,而不是所需的值1,2,3等。

有用的同步幫助程序的完整列表。 http://referencesource.microsoft.com/#q=Synchronized

系統。類別

  • 靜態的ArrayList同步(ArrayList的列表)
  • 靜態IList的同步(IList的列表)
  • 靜態Hashtable的同步(哈希表表)
  • 靜態隊列同步(隊列隊列)
  • 靜態排序列表同步(SortedList列表)
  • 靜態堆棧同步(堆棧堆棧)

System.Collections.Generic

  • 靜態IList的同步(名單列表)

System.IO

  • 靜流同步(流流)
  • 靜態TextReader同步(TextReader閱讀器)
  • 靜態的TextWriter同步(TextWriter的作家)

System.Text.RegularExpressions

  • 靜態匹配同步(匹配度內)
  • 靜態組同步(集團內)

具有正確封裝類的簡單代碼。

class Program 
{ 
    private static LogBuilder _log = new LogBuilder(); 
    static void Main(string[] args) 
    { 
     var acts = new List<Func<string>>(); 
     var rnd = new Random(); 
     for (int i = 0; i < 10000; i++) 
     { 
      int local_i = i; 
      acts.Add(() => 
      { 
       var delay = rnd.Next(5); 
       Thread.Sleep(delay); 
       return local_i.ToString() + " act that that lasted " + delay.ToString(); 
      }); 
     } 
     try 
     { 
      Parallel.ForEach(acts, act => 
      { 
       _log.WriteLine(act.Invoke()); 
      }); 
     } 
     finally 
     { 
      _log.Dispose(); 
     } 
    } 
} 

public class LogBuilder : IDisposable 
{ 
    private FileStream _fileStream; 
    private StreamWriter _streamWriter; 
    private TextWriter _synchronizedWriter; 

    public LogBuilder() 
    { 
     _fileStream = new FileStream(@"C:\temp\log.txt", FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite); 
     _streamWriter = new StreamWriter(_fileStream) { AutoFlush = true }; 
     _synchronizedWriter = TextWriter.Synchronized(_streamWriter); 
    } 
    public void WriteLine(string message) 
    { 
     _synchronizedWriter.WriteLine(message); 
    } 

    public void Dispose() 
    { 
     _streamWriter.Close(); _streamWriter.Dispose(); _fileStream.Close(); _fileStream.Dispose(); 
    } 
}