【问题标题】:Console.Out and Console.Error race condition error in a Windows service written in .NET 4.5用 .NET 4.5 编写的 Windows 服务中的 Console.Out 和 Console.Error 竞争条件错误
【发布时间】:2015-11-25 11:50:15
【问题描述】:

我在生产中遇到了一个奇怪的问题,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);
    }
}

安装程序类:

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.Write/Line() 没有任何意义。砰,问题解决了。
  • 确实,我已将 Console.Out 和 Console.Err 重定向到空流并解决了问题(一些第 3 方库直接写入它)。但是,我很好奇这是否是 Console 类的错误。示例堆栈跟踪(Console.WriteLine 似乎是内联的):System.Buffer.InternalBlockCopy(Array src, Int32 srcOffsetBytes, Array dst, Int32 dstOffsetBytes, Int32 byteCount) System.IO.StreamWriter.Write(Char[] buffer, Int32 index, Int32 count) System.IO.TextWriter.WriteLine(String value) System.IO.TextWriter.SyncTextWriter.WriteLine(String value)
  • Console.Out 是惰性创建的。这在 SyncTextWriter.WriteLine() 所依赖的 [MethodImpl(MethodImplOptions.Synchronized)] 实现上开启了可能的线程竞争。除了重新分配 Console.Out 之外,避免它的最简单方法是在 Main 方法中添加一个 Console.WriteLine() 语句。请考虑在 connect.microsoft.com 上提交此错误。
  • 谢谢你的回答,汉斯!我将记录一个错误,希望这对其他人也有帮助。

标签: c# .net console windows-services


【解决方案1】:

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

您的代码在作为 Windows 服务运行时引发异常的原因是,在这种情况下,输出和错误 TextWriters 都设置为 StreamWriter.Null,这是一个单例。您的代码同时调用 Console.WriteLine 和 Console.Error.WriteLine,当一个线程碰巧在另一个线程调用 Console.Error.WriteLine 的同时调用 Console.WriteLine 时,这会导致异常。这会导致同时从 2 个线程写入相同的流,从而导致“在复制内存时检测到可能的 I/O 竞争条件”。例外。如果你只使用Console.WriteLine或者只使用Console.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);
            }
        });
    }
}

【讨论】:

  • 我只使用 Console.WriteLine 遇到了同样的异常。仍然可能有一些库在内部使用 Console.Error.WriteLine。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-08-02
  • 2016-07-22
  • 1970-01-01
  • 2012-04-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多