'WaitForMultipleObjects always returns a pipe read handle as signaled

My question is: How to use WaitForMultipleObjects to wait until there is something the be read from an anonymous pipe?

The code below is a minimal reproducible example showing the issue with WaitForMultipleObjects which always return as signaled an anonymous pipe read handle, event if the pipe is empty.

The program create an anonymous pipe and a worker thread then read a "command" from the console. There are two commands: "exit" to quit the program and "write" to write a single byte to the pipe.

The thread call WaitForMultipleObjects to wait until the pipe read handle is signaled and when it is, it calls ReadFile to read one byte from the pipe.

The issue is that WaitForMultipleObjects always signal the pipe read handle event if the pipe is empty. The code here will then call ReadFile which will block the thread until a byte is really written to the pipe. Obviously, the thread should only be blocked in WaitForMultipleObjects (In the real application, the wait concern a lot of handles).

program PipeAndThreadDemo;

{$APPTYPE CONSOLE}

{$R *.res}

uses
    System.SysUtils, System.Classes,
    WinApi.Windows;

const
    INVALID_FILE_HANDLE      = -1;
    PIPE_CMD_NOOP            = 0;
    PIPE_CMD_TERMINATE       = 1;

type
    TPipeFd = packed record
        Read  : THANDLE;
        Write : THANDLE;
    end;
    PPipeFd = ^TPipeFd;

    TWorkerThread = class;

    TMainThread = class
    private
        FWorkerThread : TWorkerThread;
        FEventPipe    : TPipeFd;
        FThreadDone   : Boolean;
        procedure ThreadExecuteProc;
        procedure PipeCommand(Command: BYTE);
    public
        constructor Create;
        destructor  Destroy; override;
        procedure Execute;
        procedure ThreadTerminateProc;
    end;

    TWorkerThread = class(TThread)
    protected
        FParentCtrl : TMainThread;
        procedure Terminate;
    public
        destructor Destroy; override;
        procedure Execute; override;
        property ParentCtrl : TMainThread read  FParentCtrl
                                          write FParentCtrl;
    end;

var
    MainThread : TMainThread;

{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}

{ TMainThread }

{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
constructor TMainThread.Create;
begin
    inherited Create;
    FEventPipe.Read  := THANDLE(INVALID_FILE_HANDLE);
    FEventPipe.Write := THANDLE(INVALID_FILE_HANDLE);
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TMainThread.Destroy;
begin
    if Assigned(FWorkerThread) then begin
        if not FThreadDone then begin
            FWorkerThread.Terminate;
            while not FThreadDone do
                Sleep(0);
        end;
        FreeAndNil(FWorkerThread);
    end;
    if FEventPipe.Read <> THANDLE(INVALID_FILE_HANDLE) then begin
        CloseHandle(FEventPipe.Read);
        FEventPipe.Read := THANDLE(INVALID_FILE_HANDLE);
    end;
    if FEventPipe.Write <> THANDLE(INVALID_FILE_HANDLE) then begin
        CloseHandle(FEventPipe.Write);
        FEventPipe.Write := THANDLE(INVALID_FILE_HANDLE);
    end;
    inherited Destroy;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TMainThread.Execute;
var
    More : Boolean;
    Cmd  : String;
begin
    if not WinApi.Windows.CreatePipe(FEventPipe.Read, FEventPipe.Write, nil, 0) then begin
        WriteLn('Failed to create pipe.');
        Exit;
    end;

    OutputDebugString(PChar(Format('Pipe.Read=%d  Pipe.Write=%d  ThreadID=%d',
                                   [FEventPipe.Read,
                                    FEventPipe.Write,
                                    GetCurrentThreadId])));

    FWorkerThread            := TWorkerThread.Create(TRUE);
    FWorkerThread.ParentCtrl := Self;
    FWorkerThread.Start;

    WriteLn('Run the program using Delphi debugger and have a look at the events window.');

    More := TRUE;
    while More do begin
        Write('Enter command> ');
        ReadLn(Cmd);
        Cmd := Trim(Cmd);
        if SameText(Cmd, 'Exit') then
            More := FALSE
        else if SameText(Cmd, 'write') then
            PipeCommand(PIPE_CMD_NOOP)
        else
            WriteLn('Unknow command "', Cmd, '"');
    end;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TMainThread.ThreadExecuteProc;
var
    Handles   : array of THandle;
    Status    : DWORD;
    Count     : DWORD;
    Index     : DWORD;
    Ch        : BYTE;
    BytesRead : Cardinal;
begin
    OutputDebugString(PChar(Format('WorkerThread begin. ThreadID=%d',
                                   [GetCurrentThreadID])));
    Count := 1;
    SetLength(Handles, Count);
    Handles[0] := FEventPipe.Read;
    while TRUE do begin
        if Count > MAXIMUM_WAIT_OBJECTS then
            raise Exception.Create('WaitForMultipleObjects failure: too many handles');

        Status := WaitForMultipleObjects(Count, @Handles[0], FALSE, INFINITE);
        if Status = WAIT_FAILED then begin
            OutputDebugString('WaitForMultipleObjects failed');
            break;
        end;
        if (Status >= WAIT_OBJECT_0) and (Status < (WAIT_OBJECT_0 + Count)) then begin
            Index := Status - WAIT_OBJECT_0;
            OutputDebugString(PChar(Format('WaitForMultipleObjects signaled Handle %d',
                                           [Handles[Index]])));
            if Index = 0 then begin  // Check if pipe signaled
                OutputDebugString('Calling ReadFile. Should not block...');
                // <<<<===== HERE IS THE ISSUE: ReadFile block even if its handle is signaled =====>
                if not ReadFile(Handles[Index], Ch, 1, BytesRead, nil) then
                    OutputDebugString('Pipe read failed')
                else if BytesRead > 0 then begin
                    if Ch = PIPE_CMD_TERMINATE then begin
                        OutputDebugString('PIPE_CMD_TERMINATE Received');
                        break;
                    end
                    else if Ch = PIPE_CMD_NOOP then
                        OutputDebugString('PIPE_CMD_NOOP Received')
                    else
                        OutputDebugString('Unknown cmd Received');
                end
                else
                    OutputDebugString('ReadFile didn''t read anything!');
            end;
        end;
    end;
    OutputDebugString(PChar(Format('WorkerThread done. ThreadID=%d',
                                   [GetCurrentThreadID])));
    FThreadDone := TRUE;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TMainThread.ThreadTerminateProc;
begin
    PipeCommand(PIPE_CMD_TERMINATE);
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TMainThread.PipeCommand(Command: BYTE);
var
    BytesWritten : Cardinal;
begin
    if FEventPipe.Write <> INVALID_FILE_HANDLE then
        WriteFile(FEventPipe.Write, Command, 1, BytesWritten, nil);
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}

{ TWorkerThread }

{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
destructor TWorkerThread.Destroy;
begin
    inherited Destroy;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TWorkerThread.Execute;
begin
    if Assigned(FParentCtrl) then
        FParentCtrl.ThreadExecuteProc;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
procedure TWorkerThread.Terminate;
begin
    inherited Terminate;
    if Assigned(FParentCtrl) then
        FParentCtrl.ThreadTerminateProc;
end;


{* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *}
begin
    try
        MainThread := TMainThread.Create;
        try
            MainThread.Execute;
        finally
            MainThread.Free;
        end;
    except
        on E: Exception do
            Writeln(E.ClassName, ': ', E.Message);
    end;
end.


Solution 1:[1]

I've found a way to work around the fact that Windows doesn't like to wait for a pipe handle: Just create an event using CreateEvent which can be used with WaitForMultipleObjects and set the even just after having written to the pipe.

If you also need to wait on a socket, WSAWaitForMultipleEvents should be used instead of WaitForMultipleObjects and an event has to be created using WSACreateEvent and mapped to the socket events using WSAEventSelect. This socket event is manual reset so you must call WSAResetEvent once it is serviced.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 fpiette