WinForms + psql 卡死排查全纪录
Copyright Notice: This article is an original work licensed under the CC 4.0 BY-NC-ND license.
If you wish to repost this article, please include the original source link and this copyright notice.
Source link: https://v2know.com/article/1160
关键词:
ProcessStartInfo
、WaitForExit
、管道缓冲区、stdout/stderr、-w
、异步读取
背景
-
工具:自写的 WinForms “PgBackupRestoreTool”
-
功能:按钮点击 → 调用
psql -l
检测连接 -
环境:PostgreSQL 15,Windows 10
问题现象
电脑 | 行为 |
---|---|
A(旧) | 一切正常,点 Connect 秒出数据库列表 |
B(新) | 进度条一直转。任务管理器里堆着 0 % CPU 的 psql.exe |
手动在 PowerShell 中:
$env:PGPASSWORD='admin'
psql -U admin -h localhost -d bks -l # 秒连
—— 服务器与账号密码均无异常。
初步定位
-
阻塞语句
string err = p.StandardError.ReadToEnd(); // 不返回
-
子进程状态
-
psql.exe
CPU 0 % → OS 把它挂起 -
没有弹出“Password”提示
-
-
环境变量对比
没有PGPASSWORD
冲突,psql
版本皆为 15.x
排除了“密码/版本/杀毒”后,怀疑 stdout 写满无人读 → 管道死锁。
核心原因
-
psql -l
会把 所有数据库 列表写到 stdout; -
原代码只重定向 stderr,stdout 留给默认管道;
-
数据库增多后,stdout ≈ 4 KB 填满缓冲区 →
psql
被 OS 挂起; -
父进程在
ReadToEnd()
等stderr
结束,双方互卡。
解决方案
-
同时重定向 stdout & stderr
-
异步并发读取,永不堵塞
-
强制
-w
,若密码没带上立即失败 -
如不需要数据库列表,可改用
-qAt -c "SELECT 1"
,输出更小
【问题代码(Before)】
private bool RunProcessAndCheckSuccess(string exe, IEnumerable<string> args)
{
bool ok = false;
try
{
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardError = true, // 只重定向 stderr
UseShellExecute = false,
CreateNoWindow = true
};
psi.Environment["PGPASSWORD"] = PG_PASSWORD;
psi.Environment["PGCLIENTENCODING"] = "UTF8";
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = Process.Start(psi)!;
// 只读 stderr,stdout 无人读取,输出大时会堵塞
string err = p.StandardError.ReadToEnd();
p.WaitForExit();
ok = p.ExitCode == 0;
if (!ok && !string.IsNullOrEmpty(err))
Log(err.Trim(), Color.Red);
}
catch (Exception ex)
{
Log($"RunProcessAndCheckSuccess error: {ex.Message}", Color.Red);
}
return ok;
}
【修正版(After)】
private async Task<bool> RunProcessAndCheckSuccessAsync(string exe, IEnumerable<string> args)
{
try
{
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardOutput = true, // 同时重定向 stdout
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
psi.Environment["PGPASSWORD"] = PG_PASSWORD;
psi.Environment["PGCLIENTENCODING"] = "UTF8";
psi.ArgumentList.Add("-w"); // 不提示密码
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = Process.Start(psi)!;
var stdTask = p.StandardOutput.ReadToEndAsync();
var errTask = p.StandardError .ReadToEndAsync();
await Task.WhenAll(stdTask, errTask, p.WaitForExitAsync());
if (!string.IsNullOrWhiteSpace(stdTask.Result))
Log(stdTask.Result.Trim(), Color.Black);
if (p.ExitCode != 0 && !string.IsNullOrWhiteSpace(errTask.Result))
Log(errTask.Result.Trim(), Color.Red);
return p.ExitCode == 0;
}
catch (Exception ex)
{
Log($"RunProcessAndCheckSuccessAsync error: {ex.Message}", Color.Red);
return false;
}
}
【调用示例】
// 调用方法本身需为 async
bool ok = await RunProcessAndCheckSuccessAsync("psql", args);
【核心变化】
-
同时重定向 stdout 和 stderr,避免 stdout 填满管道导致死锁。
-
使用
ReadToEndAsync
+WaitForExitAsync
并发读取,UI 不再阻塞。 -
加
-w
,缺少密码时立即返回错误,不进入交互模式。 -
方法签名改为
async Task<bool>
;同步代码可用.GetAwaiter().GetResult()
强行等待。
替换后,无论数据库数量多少,工具都能瞬间完成连接测试,不再出现进度条无限旋转。
最终代码(精简版)
private async Task<bool> RunProcessAndCheckSuccessAsync(string exe, IEnumerable<string> args)
{
var psi = new ProcessStartInfo
{
FileName = exe,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
psi.Environment["PGPASSWORD"] = PG_PASSWORD;
psi.Environment["PGCLIENTENCODING"] = "UTF8";
psi.ArgumentList.Add("-w"); // no password prompt
foreach (var a in args) psi.ArgumentList.Add(a);
using var p = Process.Start(psi)!;
var stdTask = p.StandardOutput.ReadToEndAsync();
var errTask = p.StandardError .ReadToEndAsync();
await Task.WhenAll(stdTask, errTask, p.WaitForExitAsync());
if (!string.IsNullOrWhiteSpace(stdTask.Result))
Log(stdTask.Result.Trim(), Color.Black);
if (p.ExitCode != 0 && !string.IsNullOrWhiteSpace(errTask.Result))
Log(errTask.Result.Trim(), Color.Red);
return p.ExitCode == 0;
}
调用:
bool ok = await RunProcessAndCheckSuccessAsync("psql", args);
复测结果
步骤 | 电脑 A | 电脑 B |
---|---|---|
点击 Connect | <200 ms 出结果 | 同步 <200 ms 出结果 |
新建 100 个空库再测 | 仍然秒级返回 | 仍然秒级返回 |
再也没有卡死现象。
教训 & 建议
-
父子进程通信
-
只要
RedirectStandardError=true
,最好同时重定向 stdout; -
否则输出量大时极易写满缓冲区。
-
-
永远用异步 I/O
ReadToEndAsync + WaitForExitAsync
可彻底规避死锁。 -
-w
是免费保险
忘带密码时不再进入交互,而是立即返回错误。 -
问题复现越多机越快
单机调不好时,拷贝到另一台对比差异是排雷捷径。
就此,WinForms + pg 工具的“卡圈圈”事件完美收官 🏁
This article was last edited at