原文链接:Threads and Pipes in Console Apps
控制台程序中的线程和管道
问题是:如何创建一个可能阻塞的程序,但在阻塞的时候能当数据可读的时候从stdour和stderr中接收数据。本文的目的是展示如何在控制台程序中使用多线程。
子进程程序:
int _tmain(int argc, _TCHAR* argv[])


{
for(int i = 0; i < 100; i++)

{ /**//* test */
int r = rand();
if(r & 0x100)

{ /**//* stderr */
_ftprintf(stderr, _T("%4d: error\n"), i);
fflush(stderr);

} /**//* stderr */
else

{ /**//* stdout */
_ftprintf(stdout, _T("%4d: output\n"), i);
fflush(stdout);

} /**//* stdout */
int w = rand() % 500;
Sleep(200 + w);

} /**//* test */
return 0;
}

程序创建了两个线程:一个线程处理stdout,另一个线程处理stdin,主线程从两个工作线程收集数据并显示数据,当两个工作线程都结束时,主线程就结束。现在的问题是用什么机制来实现。作者使用了进程间队列机制,I/O完成端口,
class CommandLine


{
public:

CommandLine()
{ HTML = FALSE; IsUnicode = FALSE; program = NULL; }
BOOL HTML;//是否使用html
BOOL IsUnicode;//是否使用unicode
LPTSTR program;//目标程序名称
}; // class CommandLine


父进程程序:
int _tmain(int argc, _TCHAR* argv[])

{
CommandLine cmd;
for(int i = 1; i < argc; i++)

{ /**//* scan args */
CString arg = argv[i];
if(arg[0] == _T('-'))

{ /**//* option */
if(arg == _T("-u"))

{ /**//* unicode */
cmd.IsUnicode = TRUE;//使用unicode
continue;

} /**//* unicode */
if(arg == _T("-html"))

{ /**//* html */
cmd.HTML = TRUE;//使用html
continue;

} /**//* html */
_ftprintf(stderr, _T("Unrecognized option \"%s\"\n"), arg);
return Result::INVALID_ARGUMENT;

} /**//* option */
if(cmd.program != NULL)

{ /**//* two files */
_ftprintf(stderr, _T("Two command directives given:\n [1] \"%s\"\n [2]\"%s\"\n"),
cmd,
arg);
return Result::TWO_COMMANDS;

} /**//* two files */
cmd.program = argv[i];//目标程序名称

} /**//* scan args */
if(cmd.program == NULL)

{ /**//* no args */
_ftprintf(stderr, _T("need program to run\n"));
return Result::NO_PROGRAM;

} /**//* no args */
SmartHandle iocp = ::CreateIoCompletionPort(INVALID_HANDLE_VALUE, NULL, 0, 0);//创建IO完成端口

if(iocp == NULL)

{ /**//* failed iocp */
DWORD err = ::GetLastError();
_ftprintf(stderr, _T("CreateIoCompletionPort failed, error %s\n"), ErrorString(err));
return Result::IOCP_FAILED;

} /**//* failed iocp */

SECURITY_ATTRIBUTES sa =
{sizeof(SECURITY_ATTRIBUTES), NULL, TRUE};
SmartHandle stdout_read;
SmartHandle stdout_write;
SmartHandle stderr_read;
SmartHandle stderr_write;
static const UINT PIPE_BUFFER_SIZE = 32;//管道缓冲区大小
//创建管道
if(!::CreatePipe((LPHANDLE)stdout_read, (LPHANDLE)stdout_write, &sa, PIPE_BUFFER_SIZE))

{ /**//* failed stdout */
DWORD err = ::GetLastError();
_tprintf(_T("stdout pipe failure: %s\n"), ErrorString(err));
return Result::STDOUT_CREATION_FAILED;

} /**//* failed stdout */
//创建管道
if(!::CreatePipe((LPHANDLE)stderr_read, (LPHANDLE)stderr_write, &sa, PIPE_BUFFER_SIZE))

{ /**//* failed stderr */
DWORD err = ::GetLastError();
_tprintf(_T("stderr pipe failure: %s\n"), ErrorString(err));
return Result::STDERR_CREATION_FAILED;

} /**//* failed stderr */

STARTUPINFO startup =
{sizeof(STARTUPINFO)};
startup.dwFlags = STARTF_USESTDHANDLES | STARTF_USESHOWWINDOW;
startup.wShowWindow = SW_HIDE;
//子进程使用的标准输入和标准输出是父进程的写管道
startup.hStdOutput = stdout_write;
startup.hStdError = stderr_write;
PROCESS_INFORMATION procinfo;
//创建子进程,重定向标准输出和标准错误
if(!::CreateProcess(NULL, cmd.program, NULL, NULL, TRUE, CREATE_NEW_CONSOLE, NULL, NULL, &startup, &procinfo))

{ /**//* failed */
DWORD err = ::GetLastError();
_tprintf(_T("CreateProcess failed for \"%s\": %s"), cmd.program, ErrorString(err));
return Result::CREATEPROCESS_FAILED;

} /**//* failed */
::CloseHandle(procinfo.hProcess); // handle will never be needed
::CloseHandle(procinfo.hThread); // handle will never be needed
//在父进程这端关闭子进程要使用的管道端口
stdout_write.Close(); // Close our end of the pipe
stderr_write.Close(); // Close our end of the pipe
unsigned id;
//创建处理stdout的子线程
SmartHandle stdoutThread = (HANDLE)_beginthreadex(NULL, 0, reader, new ThreadParms(stdout_read, SourceFlags::StdOut, iocp, cmd.IsUnicode), 0, &id);
if(stdoutThread == NULL)

{ /**//* thread create failed */
DWORD err = ::GetLastError();
_ftprintf(stderr, _T("Thread creation for stdout failed, error %s\n"), ErrorString(err));
return Result::THREAD_FAILURE;

} /**//* thread create failed */

stdoutThread.Close(); // handle will never be used
//创建处理stderr的子线程
SmartHandle stderrThread = (HANDLE)_beginthreadex(NULL, 0, reader, new ThreadParms(stderr_read, SourceFlags::StdErr, iocp, cmd.IsUnicode), 0, &id);
if(stderrThread == NULL)

{ /**//* thread create failed */
DWORD err = ::GetLastError();
_ftprintf(stderr, _T("Thread creation for stderr failed, error %s\n"), ErrorString(err));
return Result::THREAD_FAILURE;

} /**//* thread create failed */
stderrThread.Close(); // handle will never be used
SourceFlags::FlagType broken = SourceFlags::None;
Result::Type result = Result::SUCCESS;
while(broken != (SourceFlags::StdOut | SourceFlags::StdErr))

{ /**//* watch pipes */
//异步方式监控管道
OVERLAPPED * ovl;
DWORD bytesRead;
ULONG_PTR key;
//获取完成端口队列状态,获取从两个子线程到来的数据,key表明来自哪个线程已经完成了I/O操作,bytesRead表明数据来源
BOOL ok = ::GetQueuedCompletionStatus(iocp, &bytesRead, &key, &ovl, INFINITE);
if(!ok)

{ /**//* failed */
DWORD err = ::GetLastError();
result = Result::IOCP_ERROR;
_ftprintf(stderr, _T("GetQueuedCompletionStatus failed, error %s\n"), ErrorString(err));
break;

} /**//* failed */
broken = (SourceFlags::FlagType)(broken | (int)key);
//终止信号到来
if(key != 0)
continue; // termination notifications contain no data
//输出数据
CString * s = (CString *)ovl;
WriteToOutput(*s, (SourceFlags::FlagType)bytesRead, cmd);
delete s;

} /**//* watch pipes */
stdout_read.Close();
stderr_read.Close();
return result;
} // _tmain

智能句柄类
用来处理句柄的生存期问题:
class SmartHandle


{//智能句柄类
public:

SmartHandle()
{ handle = NULL; }

SmartHandle(HANDLE h)
{ handle = h; }

virtual ~SmartHandle()
{ Close();}
public:

operator HANDLE()
{ return handle; }

operator LPHANDLE()
{ return & handle; }

bool operator==(HANDLE h)
{ return handle == h; }

SmartHandle & operator=(HANDLE h)
{ handle = h; return *this; }
public:

void Close()
{ if(handle != NULL) ::CloseHandle(handle); handle = NULL; }
protected:
HANDLE handle;
};

线程参数类:
class ThreadParms


{//线程参数
public:
ThreadParms(HANDLE h, SourceFlags::FlagType f, HANDLE io, BOOL uni)

{
stream = h;
flags = f;
iocp = io;
IsUnicode = uni;
}
public:
HANDLE stream;//管道读端口
SourceFlags::FlagType flags;//指明数据源
HANDLE iocp;//I/O完成端口
BOOL IsUnicode;//是否使用unicode
};

DWORD NumberOfBytesTransferred
|
ULONG_PTR CompletionKey
|
LPOVERLAPPED Overlapped
|
Meaning
|
SourceFlags::StdOut
|
0
|
(LPOVERLAPPED)(CString *)
|
stdout line to display
|
SourceFlags::StdErr
|
0
|
(LPOVERALLPED)(CString *)
|
stderr line to display
|
0
|
SourceFlags::StdOut
|
NULL
|
stdout has terminated
|
0
|
SourceFlags::StdErr
|
NULL
|
stderr has terminated
|
读线程工作函数
UINT __stdcall reader(LPVOID p)

{//读线程工作函数
ThreadParms * parms = (ThreadParms *)p;
PipeReader pipe(parms->stream, parms->IsUnicode);//创建管道读者
CString Prefix;
while(TRUE)

{ /**//* processing loop */
//读管道数据
if(!pipe.Read())

{ /**//* failed stream */
break;

} /**//* failed stream */
FormatAndOutput(pipe.GetString(), Prefix, parms);

} /**//* processing loop */
if(!Prefix.IsEmpty())

{ /**//* write out last line */
CString text(_T("\r\n"));
FormatAndOutput(text, Prefix, parms);

} /**//* write out last line */
//posts an I/O completion packet to an I/O completion port
//发出完成信息,没有数据,因此第二个参数为,第三个参数表明完成的是哪个
::PostQueuedCompletionStatus(parms->iocp, 0, parms->flags, NULL);
delete parms;
return 0;
} // reader
class PipeReader


{//管道读者
protected:
static const UINT MAX_BUFFER = 1024;//缓存大小
public:

PipeReader(HANDLE str, BOOL uni)
{ Init(); stream = str; IsUnicode = uni; }
CString GetString()

{
if(IsUnicode)
return CString((LPCWSTR)buffer);
else
return CString((LPCSTR)buffer);
}
BOOL Read()

{//从管道中读取数据
if(Offset == 1)
buffer[0] = reread;
if(!ReadFile(stream, &buffer[Offset], MAX_BUFFER - (IsUnicode ? sizeof(WCHAR) : sizeof(char)), &bytesRead, NULL))
return FALSE;
if(IsUnicode)

{ /**//* unicode pipe */
if((Offset + bytesRead) & 1)

{ /**//* odd bytes read */
Offset = 1; // offset for next read
reread = buffer[Offset + bytesRead - 1]; // force reread
buffer[Offset + bytesRead - 1] = 0; // remove from current buffer
bytesRead--; // pretend we didn't see it

} /**//* odd bytes read */
else

{ /**//* even bytes read */
Offset = 0; // offset for next read

} /**//* even bytes read */
buffer[Offset + bytesRead] = 0;
buffer[Offset + bytesRead + 1] = 0; // create Unicode NUL

} /**//* unicode pipe */
else

{ /**//* ANSI pipe */
buffer[bytesRead] = '\0';

} /**//* ANSI pipe */
return TRUE;
} // PipeReader::Read
protected:

void Init()
{ stream = NULL; Offset = 0; IsUnicode = FALSE; }
BOOL IsUnicode;//是否使用unicode
HANDLE stream;//管道读端口
protected:
BYTE buffer[MAX_BUFFER];//缓冲区
DWORD Offset;
BYTE reread;
DWORD bytesRead;
}; // class PipeReader

小结
子进程往管道的两个写端口写数据,父进程从管道的两个读端口读数据,为了读数据,父进程创建了两个子线程,一个子线程从stdout中读数据,一个子线程从stderr中读数据,为了能异步读取数据,使用了I/O完成端口。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述