locked
NamedPipeClientStream.Connect causes CPU Hang. Is there a workaround?

    General discussion

  •   
    NamedPipeClientStream.Connect causes CPU Hang. Is there a workaround?

    PROBLEM DEFINITION
     
    Under normal usage the code as written can cause livelock, where the CPU usage goes to 50% or higher per outstanding connection request - I have observed usage rates approaching 100%, depending on how many simultaneous connection attempts were in progress.
     
    The snippet shown here is the relevant portion of the code in System.IO.Pipes (from Reflector).
     
    public void Connect(int timeout)
    {
        // ... other code here
        int startTickCount = Environment.TickCount;
        int elapsed = 0;
        while ((timeout == -1) || ((elapsed = Environment.TickCount - startTickCount) < timeout))
        {
          if ( !UnsafeNativeMethods.WaitNamedPipe(this.m_normalizedPipePath, timeout - elapsed) )
          {
              int errorCode = Marshal.GetLastWin32Error();
              if (errorCode == 2 ) // pipe does not exist
                continue;
              else if (errorCode == 0 ) // timeout occurred in WaitNamedPipe
                break; // done
              __Error.WinIOError(errorCode, string.Empty);
          }
          //... other code here...
          return;
        }
      throw new TimeoutException();
    }
     
    CODE ANALYSIS

    The source of the problem is the call to WaitNamedPipe(). The Win32 docuemntation states that it returns when an instance of the pipe becomes available or a timoeut occurs. However, the remarks section have an additional comment: "If no instances of the specified pipe exist, the WaitNamedPipe function returns immediately, regardless of time-out value."
     
    In other words, if a named pipe instance exists and a client attempts to connect, the method will wait until either the server accepts the connection request or it times out, so that a timeout will occur only if the server takes too long to wake up and accept the connection - this is the expected behavior.

    But when the pipe does not exist at all WaitNamedPipe does not wait, it just returns immediately with an error code; perhaps the server has not started yet, or is not at a point where it can create a pipe.
     
    When this occurs, the NamedPipeClientStream.Connect code goes into a tight loop that spins around calls to WaitNamedPipe; it continually polls WaitNamedPipe until the elapsed time exceeds the timeout value and the method throws a timeout exception; or if the timeout is specified as -1, it will spin until the process exits or the thread is aborted. This causes livelock/CPU hang.
     
    The developer who wrote this obviously knew this could occur since the code explicitly handles that case, but it was not documented that it could behave this way.

    Bottom line - perhaps I am mistaken, but from my perspective, when the API is used as-intended it produces undesirable results.

     
    SOLUTIONS?
     
    So, as to workarounds and other issues....

    I'd prefer a solution where the connecting thread blocks (waits on an event) until either a pipe is available or a timeout occurs.

    My current workaround is to call Connect with a short timeout period and then if Connect fails with a Timeout exception, to sleep for a longer period and then retry. This reduces the livelock problem to a shorter time but it does not eliminate it.

    Is there a way to determine if a named pipe exists? If there was, then I could check if it existed before calling Connect, and if it did not, I could sleep for a while and check again. Since named pipes are implemented as a file system, perhaps a call to OpenFile? The ideal solution would be to block until the pipe (file) was created, and then wake the thread.
    Another possibility is to call WaitNamedPipe with a timeout of 0 and if the return value is 0 use this to indicate that the pipe had not been created.

    This might only work when the named pipe is on the same machine as the client, but since that's my use case it's the one I am most interested in.
     
    Does anyone have any advice here?


    As a suggestion to MSFT, I'd also prefer some other implementation options,

    First, use an algorithm that does not cause livelock.

    Second, if the pipe does not exist then return immediately with a suitable error code, or an exception, that makes it obvious what the problem is. This is a different case then the server existing but not responding in a timely manner.

    Third, an option for returning a value of false when it times out rather than throw an exception, and return true if it succeeds. Timeouts here are not necessarily errors and should not throw exceptions.

    Thanks



    Tuesday, January 13, 2009 1:01 PM

All replies

  • Since no one has replied I'll post my own solution to the problem in case anyone googles for the same problem.

     To recap, the problem was that calling Connect with a non-zero timeout and when the server had not yet created the pipe caused the BCL routine to sit in a spin loop, burning CPUs. The workaround was to write a routine that checked for the existence of the named pipe before calling Connect. If the pipe did not exist then the caller would sleep, block, or do other useful work for a while before checking again.

    Here's the code that check for the existence of the server's named pipe.

      [return: MarshalAs( UnmanagedType.Bool )]
      [DllImport( "kernel32.dll", CharSet=CharSet.Auto, SetLastError=true )]
      private static extern bool WaitNamedPipe( string name, int timeout );


      /// <summary>
      /// Provides an indication if the named pipe exists.
      /// This has to prove the pipe does not exist. If there is any doubt, this
      /// returns that it does exist and it is up to the caller to attempt to connect
      /// to that server. The means that there is a wide variety of errors that can occur that
      /// will be ignored - for example, a pipe name that contains invalid characters will result
      /// in a return value of false.
      ///
      /// </summary>
      /// <param name="pipeName">The pipe to connect to</param>
      /// <returns>false if it can be proven it does not exist, otherwise true</returns>
      /// <remarks>
      /// Attempts to check if the pipe server exists without incurring the cost
      /// of calling NamedPipeClientStream.Connect. This is because Connect either
      /// times out and throws an exception or goes into a tight spin loop burning
      /// up cpu cycles if the server does not exist.
      ///
      /// Common Error codes from WinError.h
      /// ERROR_FILE_NOT_FOUND 2L
      /// ERROR_BROKEN_PIPE =  109 (0x6d)
      /// ERROR_BAD_PATHNAME  161L The specified path is invalid.
      /// ERROR_BAD_PIPE =  230  (0xe6) The pipe state is invalid.
      /// ERROR_PIPE_BUSY =  231 (0xe7) All pipe instances are busy.
      /// ERROR_NO_DATA =   232   (0xe8) the pipe is being closed
      /// ERROR_PIPE_NOT_CONNECTED 233L No process is on the other end of the pipe.
      /// ERROR_PIPE_CONNECTED        535L There is a process on other end of the pipe.
      /// ERROR_PIPE_LISTENING        536L Waiting for a process to open the other end of the pipe.
      ///
      /// </remarks>
      static public bool NamedPipeDoesNotExist( string pipeName )
      {
       try
       {
        int timeout = 0;
        string normalizedPath = System.IO.Path.GetFullPath(
         string.Format( @"\\.\pipe\{0}", pipeName ) );
        bool exists = WaitNamedPipe( normalizedPath, timeout );
        if ( !exists )
        {
         int error = Marshal.GetLastWin32Error();
         if ( error == 0 ) // pipe does not exist
          return true;
         else if ( error == 2 ) // win32 error code for file not found
          return true;
         // all other errors indicate other issues
        }
        return false;
       }
       catch ( Exception ex )
       {
        Exception.Publish( "Failure in WaitNamedPipe()", ex );
        return true; // assume it exists
       }
      }
     

    Friday, January 30, 2009 8:48 PM
  • I just ran into this exact same problem, 100% CPU (50% on a dual core) on wait connect for a pipe that does not yet exist.
    Googled for it and found this. I'll try the workaround, many thanks David.

    But what I'd really like to see is a comment from someone on the BCL team please. Feels bad enough to warrant a patch to me.

    Edward
    Thursday, April 9, 2009 2:40 PM
  • Hi David

    I also had this problem of hang on connect when server is absent. Your solution worked perfectly for me after I

    (1) added:

    using System.Runtime.InteropServices;

    and (2) changed

    Exception.Publish( "Failure in WaitNamedPipe()", ex );

    to match my exception handler.

    Thanks for this.

    But I am astonished that an error as apparently fundamental as this hang on connect still exists.
    Thursday, June 18, 2009 2:21 PM
  • Nice work on this David, thanks for the analysis and workaround. I was blaming my old laptop for poor performance (100% CPU) when it was really the API behavior.
    Hope MS does make a patch for this.

    I translated the workaround to VB for others' convenience, and added the code snippet of how I use the workaround in a VB2008 Module.

    RichR
    ' This is a formal declaration using DLLIMPORT and MarshalAs.
    '<DllImport("KERNEL32.DLL", SetLastError:=True, CharSet:=CharSet.Auto)> _
        'Public Shared Function WaitNamedPipe( _
        'ByVal sPipeName As String, _
        'ByVal nTimeOut As Integer) _
        'As <MarshalAsAttribute(UnmanagedType.Bool)> Boolean
        '    ' Leave the body of the function empty.
        'End Function 
    
    ' This is the traditional VB declaration. 
    ' 'Auto' here is the same as 'CharSet.Auto' (above), 
    ' so we don't have to specify an alias of WaitNamedPipeA or WaitNamedPipeW.
      Declare Auto Function WaitNamedPipe Lib "Kernel32.dll" (ByVal sPipeName As String, ByVal nTimeOut As Integer) As Boolean
    
    
       Function NamedPipeDoesNotExist(ByVal sPipeName As String) As Boolean
            Try
                Dim timeout As Integer = 0
                Dim normalizedPath As String = Path.GetFullPath(String.Format("\\.\pipe\{0}", sPipeName))
                Dim exists As Boolean = WaitNamedPipe(normalizedPath, timeout)
    
                If Not exists Then
                    Dim Myerror As Integer = Marshal.GetLastWin32Error()
                    ' For your Debug pleasure                
                    ' Console.WriteLine(Now() & " MyError: " & Myerror)
                    ' Debug END
                    If (Myerror = 0) Then
                        Return True           ' pipe does not exist
                    ElseIf (Myerror = 2) Then ' win32 error code for file not found
                        Return True
                    End If
                    Return True               ' all other errors indicate other issues
                Else
                    ' Found pipe, all is OK.
                    Return False
                End If
            Catch ex As Exception
                Console.WriteLine("Failure in WaitNamedPipe() " & ex.Message)
                Return True 'can't use it.
            End Try
        End Function
    
    ===================
    ' ---------| usage |----------
    ' I wrapped the function around a 1 second delay,
    ' which considerably reduced the CPU load.
    ' This is from a VB Module.
    
    Do
         Threading.Thread.Sleep(1000)
    Loop While (NamedPipeDoesNotExist("MyPipeNameGoesHere"))
    pipeClient.Connect()
    ...
    
    
    Wednesday, September 2, 2009 2:21 PM
  • This workaround worked for us thank you so much!

     

    We used the pipes to know if we had another application running on the same machine so we looped until we got the pipe server up and running.

     

    We reversed the logic in the method to be DoesNamedPipeExist.

     

        private void Connect()
        {
          if (_pipeClient != null)
          {
            while (true)
            {
              try
              {
                if (DoesNamedPipeExist(_pipeEndpoint))
                {
                  _pipeClient.Connect(500);
                  break;
                }
                Thread.Sleep(1000);
    
              }
              catch (Exception)
              {
                //nothing
                Thread.Sleep(1000);
              }
            }
            _isConnected = true;
            InvokeConnected();
            ReadStream();
          }
        }

     

     

    Friday, February 25, 2011 10:14 PM
  • For what its worth, on Win Server 2003, this .net program will send a core to 100% for ever...  Win7 does not have this problem though

     

    new namedPipeClientStream("FRED").Connect();


    Dave A
    Wednesday, November 30, 2011 4:30 AM
  • i also faced the same problem of high cpu usage
    and my solution will be like 

     # region NamedPipes
    
    
            public static string rcv_via_pipe(string server_name, string pipe_name)
            {
                string temp = "", temp1 = "";
                if (server_name.Length == 0)
                    server_name = ".";
                using (NamedPipeServerStream pipeServer =
                    new NamedPipeServerStream(pipe_name, PipeDirection.InOut))
                {
                    //Console.WriteLine("NamedPipeServerStream object created.");
    
                    // Wait for a client to connect
                    //Console.Write("Server Waiting for client connection...");
                    pipeServer.WaitForConnection();
                    
                    //Console.WriteLine("Client connected.");
                    try
                    {
                        // Read user input and send that to the client process.
                        using (StreamReader sr = new StreamReader(pipeServer))
                        {
                            // Display the read text to the console
    
                            while ((temp = sr.ReadLine()) != null)
                            {
                                temp1 += temp;
                            }
                        }
                      
                    }
                    // Catch the IOException that is raised if the pipe is 
                    // broken or disconnected.
                    catch (IOException e)
                    {
                        Console.WriteLine("ERROR: {0}", e.Message);
                    }
                }
                return temp1;
            }
    
            public static string send_via_pipe(string msg, string server_name,string pipename)
            {
                string temp = "", temp1 = "";
                if (server_name.Length == 0)
                    server_name = ".";
                using (NamedPipeClientStream pipeClient =
                    new NamedPipeClientStream(server_name, pipename, PipeDirection.InOut))
                {
    
                    // Connect to the pipe or wait until the pipe is available.
                    //Console.Write("Client Attempting to connect to pipe...");
                    try
                    {
                    pipeClient.Connect(500);
    
                    //Console.WriteLine("Connected to pipe.");
                    //Console.WriteLine("There are currently {0} pipe server instances open.",pipeClient.NumberOfServerInstances);
                    using (StreamWriter sw = new StreamWriter(pipeClient))
                    {
                        sw.AutoFlush = true;
    
                        sw.WriteLine(msg);
                    }
    
                    }
                    catch (Exception ex)
                    {
                        Console.WriteLine(ex. Message);
                    }
                }
                //Console.Write("Press Enter to continue...");
                //Console.ReadLine();
                return temp1;
            }
    
            # endregion NamedPipes
    

    i don't know that if im correct but the solution just works... rit

    would love to hear from you :)
    mail : jazeem10@live.com

    Wednesday, November 28, 2012 5:31 PM