在.NET 4.5中编写的Windows服务中出现Console.Out和Console.Error竞争条件错误
本文关键字:Console Out 错误 Error 竞争 条件 服务 NET Windows | 更新日期: 2023-09-27 18:24:33
我在生产中遇到了一个奇怪的问题,一个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.的了解
我创建了一个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
{
/// <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);
}
}
安装程序类别:
partial class ProjectInstaller
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Component Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
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是不同流的TextWriter的情况。
代码在作为Windows服务运行时引发异常的原因是,在这种情况下,输出和错误TextWriters都设置为StreamWriter.Null,这是一个单例。您的代码同时调用Console.WriteLine和Console.Error.WriteLine,当一个线程恰好在另一个线程调用Console.Error.WriteLine的同时调用Console.WriteLine时,会导致异常。这会导致同一流同时从两个线程写入,从而导致"复制内存时检测到可能的I/O竞争条件"异常。如果只使用Console.WriteLine或只使用Consol.Error.WriteLine,则会发现异常不再发生。
这里有一个最小的非服务控制台程序来演示这个问题:
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);
}
});
}
}