'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 |