Console.Out and Console.Error race condition error in a Windows service written in .NET 4.5

不羁岁月 提交于 2019-12-09 15:20:11

问题


I have hit a weird issue in production with a windows service hanging randomly and would appreciate any help with the root cause analysis.

The service is written in C# and is deployed to a machine with .NET 4.5 (although I am able to reproduce it with .NET 4.5.1 as well).

The error reported is:

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.

I have narrowed down the source of the exception to calls to Console.WriteLine() and Console.Error.WriteLine() in a logger. These get called from multiple threads and under high load, the error starts to appear and the service hangs.

However, according to MSDN the whole Console class is thread-safe (and I've used it before from multiple threads, no issues). What's more, this problem does not appear when running the same code as a console application; only from a windows service. And last, the stack trace for the exception shows an internal call to SyncTextWriter in the console class which should be the synchronized version mentioned in the exception.

Does anyone know if I am doing something wrong or missing a point here? A possible workaround seems to be redirecting the Out and Err streams to /dev/null but I'd prefer a more detailed analysis which seems beyond my knowledge of .NET.

I've created a repro windows service that throws the error when tried. Code is below.

Service class:

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

Main class:

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

Installer class:

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

Installing this service with InstallUtil.exe and starting it logs the errors in the event log.


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


来源:https://stackoverflow.com/questions/33915790/console-out-and-console-error-race-condition-error-in-a-windows-service-written

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!