2015-11-25 26 views
8

Uderzyłem dziwny problem w produkcji z usługą Windows wiszącą losowo i doceniłbym każdą pomoc z analizy przyczyny źródłowej.Console.Out i Console.Error błąd warunku wyścigu w usłudze Windows napisanej w .NET 4.5

Usługa jest napisana w języku C# i jest wdrażana na komputerze z .NET 4.5 (chociaż jestem w stanie odtworzyć go również z .NET 4.5.1).

Błąd zgłaszane jest:

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. 

mam zawężony źródło wyjątkiem zaproszeń do Console.WriteLine() i Console.Error.WriteLine() w rejestratorze. Są one wywoływane z wielu wątków i pod dużym obciążeniem, błąd zaczyna się pojawiać i usługa zawiesza się.

Jednak zgodnie z MSDN cała klasa konsoli jest wątkowo bezpieczna (i używałem jej wcześniej z wielu wątków, bez problemów). Co więcej, ten problem pojawia się nie przy uruchomieniu tego samego kodu co aplikacja konsoli; tylko z usługi Windows. I na koniec, ślad stosu dla wyjątku pokazuje wewnętrzne wywołanie SyncTextWriter w klasie konsoli, która powinna być zsynchronizowaną wersją wspomnianą w wyjątku.

Czy ktoś wie, czy robię coś złego, czy brakuje tu punktu? Możliwym rozwiązaniem wydaje się być przekierowanie strumieni Out i Err do/dev/null, ale wolę bardziej szczegółową analizę, która wydaje się poza moją wiedzą o .NET.

Utworzono usługę repro Windows, która zgłasza błąd podczas próby. Kod znajduje się poniżej.

klasa

Usługa:

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

główne klasy:

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

Instalator klasa:

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

Zainstalowanie tej usługi InstallUtil.exe i rozpoczęcie rejestruje błędy w przypadku log.

+0

ciemno zgadywać bez śladu stosu.Usługa nie ma konsoli, więc nie ma sensu wywoływać Console.Write/Line(). Bang, problem rozwiązany. –

+0

Rzeczywiście, przekierowałem Console.Out i Console.Err do strumienia null i to rozwiązuje problem (niektóre biblioteki stron trzecich zapisują do niego bezpośrednio). Jednak jestem ciekawy, czy jest to błąd w klasie Console, czy nie. Ślad próbki stos (Console.WriteLine wydaje się być wstawiane) 'System.Buffer.InternalBlockCopy (tablica SRC Int32 srcOffsetBytes Array DST Int32 dstOffsetBytes, Int32 byteCount) System.IO.StreamWriter.Write (char [] bufor, indeks Int32, liczba Int32) System.IO.TextWriter.WriteLine (wartość ciągu) System.IO.TextWriter.SyncTextWriter.WriteLine (wartość ciągu) ' – braintechd

+0

Konsola.Out tworzy się leniwie. To otwiera możliwy wyścig gwintowania na implementacji [MethodImpl (MethodImplOptions.Synchronized)], na której opiera się SyncTextWriter.WriteLine(). Najprostszym sposobem uniknięcia tego, poza ponownym przypisaniem konsoli. Wystarczy, że dodasz instrukcję Console.WriteLine() w swojej głównej metodzie. Czy rozważenie zgłoszenia tego błędu na connect.microsoft.com btw. –

Odpowiedz

10

Console.Out i Console.Error są bezpieczne dla wątków, ponieważ zwracają zabezpieczone wątkiem opakowanie (przez TextWriter.Synchronized) dla wyjścia konsoli i strumienia błędów TextWriters. Jednak to zabezpieczenie wątku ma zastosowanie tylko wtedy, gdy Console.Out i Console.Error są TextWriters dla różnych strumieni.

Powodem, dla którego twój kod zgłasza wyjątek podczas działania jako usługa systemu Windows, jest to, że w tym przypadku tekst wyjściowy i błąd TextWriters są ustawione jako StreamWriter.Null, co jest pojedynczą jednostką. Twój kod wywołuje Console.WriteLine i Console.Error.WriteLine i powoduje to wyjątek, gdy jeden wątek zadzwoni do Console.WriteLine w tym samym momencie, w którym inny wątek wywołuje Console.Error.WriteLine. Powoduje to, że ten sam strumień zostanie zapisany z 2 wątków w tym samym czasie, co spowoduje wykrycie "Prawdopodobnego stanu wyścigu I/O podczas kopiowania pamięci". wyjątek. Jeśli korzystasz tylko z Console.WriteLine lub korzystasz tylko z Console.Error.WriteLine, odkryjesz, że wyjątek już nie występuje.

Oto minimalne Program Konsola non-usługa, która demonstruje problem:

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