問題描述
用 .NET 4.5 編寫的 Windows 服務中的 Console.Out 和 Console.Error 競爭條件錯誤 (Console.Out and Console.Error race condition error in a Windows service written in .NET 4.5)
我在生產中遇到了一個奇怪的問題,Windows 服務隨機掛起,希望能在根本原因分析方面提供任何幫助。
該服務是用 C# 編寫的,並部署到使用 .NET 4.5 的機器上(儘管我也可以使用 .NET 4.5.1 重現它)。
錯誤報告的是:
Probable I/O race condition detected while copying memory.
The I/O package is not thread safe by default.
In multithreaded applications, a stream must be accessed in a thread‑safe way, such as a thread‑safe wrapper returned by TextReader's or TextWriter's Synchronized methods.
This also applies to classes like StreamWriter and StreamReader.
我已將異常的來源縮小到在記錄器中調用 Console.WriteLine() 和 Console.Error.WriteLine()。這些從多個線程調用並且在高負載下,開始出現錯誤並且服務掛起。
但是,根據 MSDN 整個 Console 類是線程安全的(我之前在多個線程中使用過它,沒有問題)。更重要的是,這個問題不會在和控制台應用程序運行相同的代碼時出現;僅來自 Windows 服務。最後,異常的堆棧跟踪顯示了在控制台類中對 SyncTextWriter 的內部調用,這應該是異常中提到的同步版本。
有誰知道我做錯了什麼或遺漏了一點這裡?一種可能的解決方法似乎是將 Out 和 Err 流重定向到 /dev/null 但我更喜歡更詳細的分析,這似乎超出了我對 .NET 的了解。
我創建了一個重現 Windows 服務嘗試時會引發錯誤。代碼如下。
服務類:
[RunInstaller(true)]
public partial class ParallelTest : ServiceBase
{
public ParallelTest()
{
InitializeComponent();
this.ServiceName = "ATestService";
}
protected override void OnStart(string[] args)
{
Thread t = new Thread(DoWork);
t.IsBackground = false;
this.EventLog.WriteEntry("Starting worker thread");
t.Start();
this.EventLog.WriteEntry("Starting service");
}
protected override void OnStop()
{
}
private void DoWork()
{
this.EventLog.WriteEntry("Starting");
Parallel.For(0, 1000, new ParallelOptions() { MaxDegreeOfParallelism = 10 }, (_) =>
{
try
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("test message to the out stream");
Thread.Sleep(100);
Console.Error.WriteLine("Test message to the error stream");
}
}
catch (Exception ex)
{
this.EventLog.WriteEntry(ex.Message, EventLogEntryType.Error);
//throw;
}
});
this.EventLog.WriteEntry("Finished");
}
}
主類:
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
static void Main()
{
// Remove comment below to stop the errors
//Console.SetOut(new StreamWriter(Stream.Null));
//Console.SetError(new StreamWriter(Stream.Null));
ServiceBase[] ServicesToRun;
ServicesToRun = new ServiceBase[]
{
new ParallelTest()
};
ServiceBase.Run(ServicesToRun);
}
}
安裝程序類:
參考解法
方法 1:
Console.Out and Console.Error are both thread‑safe as they each return a thread‑safe wrapper (via TextWriter.Synchronized) for the console output and error stream TextWriters. However, this thread‑safety only applies if Console.Out and Console.Error are TextWriters for different streams.
The reason that your code throws an exception when it runs as a Windows service is that in that case, the output and error TextWriters are both set to StreamWriter.Null, which is a singleton. Your code calls both Console.WriteLine and Console.Error.WriteLine and this causes the exception when one thread happens to call Console.WriteLine at the same time that another thread is calling Console.Error.WriteLine. This causes the same stream to be written to from 2 threads at the same time, resulting in the "Probable I/O race condition detected while copying memory." exception. If you only use Console.WriteLine or only use Console.Error.WriteLine, you'll discover that the exception no longer occurs.
Here's a minimal non‑service console program that demonstrates the issue:
using System;
using System.IO;
using System.Threading.Tasks;
class Program
{
static void Main(string[] args)
{
var oldOut = Console.Out;
var oldError = Console.Error;
Console.SetOut(StreamWriter.Null);
Console.SetError(StreamWriter.Null);
Parallel.For(0, 2, new ParallelOptions() { MaxDegreeOfParallelism = 2 }, (_) =>
{
try
{
while(true)
{
Console.WriteLine("test message to the out stream");
Console.Error.WriteLine("Test message to the error stream");
}
}
catch (Exception ex)
{
Console.SetOut(oldOut);
Console.SetError(oldError);
Console.WriteLine(ex);
Environment.Exit(1);
}
});
}
}
(by braintechd、user148673)