用.NET 4.5编写的Windows服务中的Console.Out和Console.Error争用条件错误

我已经在生产过程中遇到了一个奇怪的问题,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. 

我已经缩小了exception的来源,以便在记录器中调用Console.WriteLine()和Console.Error.WriteLine()。 这些从多个线程调用,在高负载下,错误开始出现,服务挂起。

但是,根据MSDN ,整个Console类是线程安全的(我之前从多个线程使用它,没有问题)。 更重要的是,当运行与控制台应用程序相同的代码时, 不会出现此问题; 只能从Windows服务。 最后,exception的堆栈跟踪显示了对控制台类中的SyncTextWriter的内部调用,该调用应该是exception中提到的同步版本。

有谁知道我做错了什么或错过了一点吗? 一个可能的解决方法似乎是将Out和Err流重定向到/ dev / null,但我更喜欢更详细的分析,这似乎超出了我的.NET知识。

我创建了一个repro 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 { ///  /// The main entry point for the application. ///  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); } } 

安装程序类:

 partial class ProjectInstaller { ///  /// Required designer variable. ///  private System.ComponentModel.IContainer components = null; ///  /// Clean up any resources being used. ///  /// true if managed resources should be disposed; otherwise, false. protected override void Dispose(bool disposing) { if (disposing && (components != null)) { components.Dispose(); } base.Dispose(disposing); } #region Component Designer generated code ///  /// Required method for Designer support - do not modify /// the contents of this method with the code editor. ///  private void InitializeComponent() { this.serviceProcessInstaller1 = new System.ServiceProcess.ServiceProcessInstaller(); this.serviceInstaller1 = new System.ServiceProcess.ServiceInstaller(); // // serviceProcessInstaller1 // this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; this.serviceProcessInstaller1.Password = null; this.serviceProcessInstaller1.Username = null; // // serviceInstaller1 // this.serviceInstaller1.ServiceName = "ATestServiceHere"; // // ProjectInstaller // this.Installers.AddRange(new System.Configuration.Install.Installer[] { this.serviceProcessInstaller1, this.serviceInstaller1}); } #endregion private System.ServiceProcess.ServiceProcessInstaller serviceProcessInstaller1; private System.ServiceProcess.ServiceInstaller serviceInstaller1; } 

使用InstallUtil.exe安装此服务并启动它会在事件日志中记录错误。

Console.Out和Console.Error都是线程安全的,因为它们每个都为控制台输出和错误流TextWriters返回一个线程安全的包装器(通过TextWriter.Synchronized)。 但是,如果Console.Out和Console.Error是不同流的TextWriters,则此线程安全性仅适用。

您的代码在作为Windows服务运行时抛出exception的原因是,在这种情况下,输出和错误TextWriters都设置为StreamWriter.Null,这是一个单例。 您的代码调用Console.WriteLine和Console.Error.WriteLine,当一个线程在另一个线程正在调用Console.Error.WriteLine的同时调用Console.WriteLine时,这会导致exception。 这会导致同时从2个线程写入相同的流,从而导致“复制内存时检测到可能的I / O竞争条件”。 例外。 如果您只使用Console.WriteLine或仅使用Console.Error.WriteLine,您将发现不再出现该exception。

这是一个最小的非服务控制台程序,它演示了这个问题:

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