一台电脑只有1个物理 COM 口,如果在两个地方(主程序 + 子页面)都写了 new SerialPort("COM1")
-
结果:
-
谁先
Open()
,谁就占坑成功; -
后一个再
Open()
会报 "端口已被占用"。
-
-
于是出现"程序 A 连上了,程序 B 就连不上"的尴尬。
单例 + 端口复用
1️⃣ 单例(Singleton)
-
整个进程里只 new 一次 SerialPort。
-
任何地方都用 同一个对象 → 不会重复占用端口。
-
代码里用
Lazy<T>
或static
变量即可: -
csharp
复制
csprivate static readonly Lazy<Powersupple_AE0> _lazy = new(() => new Powersupple_AE0()); public static Powersupple_AE0 Instance => _lazy.Value;
2️⃣ 端口复用(Port Reuse)
-
虽然只有一个串口对象,但 端口号可能想换(COM1→ COM2)。
-
复用逻辑:
-
端口相同 → 直接返回已打开的串口;
-
端口不同 → 先关闭旧串口,再 new 一个指向新端口。
-
-
csharp
复制
cspublic void Open(string portName = "COM1", byte address = 1) { lock (this) { if (_serial?.IsOpen == true && _serial.PortName == portName) return; // ✅ 端口没变,直接复用 _serial?.Close(); // ❌ 端口变了或没开,先关 _serial?.Dispose(); _serial = new SerialPort(portName, 4800, ...); _serial.Open(); _serial.WriteLine("..."); } }
"单例"保证整个进程只 new 一次串口,"端口复用"保证即使换端口,也只保留一个活的连接。
cs
// 第一次:真正打开
Powersupple_AE800.Instance.Open("COM25", 1);
// 第二次:直接复用
Powersupple_AE800.Instance.SetVoltage(12);
// 换端口:先关旧再开新
Powersupple_AE800.Instance.Open("COM26", 1);
在多个线程里共用同一个串口单例时
核心只有一句话:"串口不是线程安全的,必须串行化访问。"
1. 所有读写必须加锁(最简单也最安全)
所有 SendCommand
/ Open
/ Close
都用它包裹即可:
cs
private readonly object _lock = new object();
public void SetVoltage(double v)
{
lock (_lock)
{
SendCommand($"命令");
}
}
UI 线程、后台线程、定时器线程......无论谁调 ,都走同一把锁,绝不会交叉写入。
2. 不要跨线程直接访问 SerialPort
成员
错误示例
cs
Task.Run(() => ae800.serialPort.WriteLine("xxx")); // 可能和 UI 线程冲突
正确做法
cs
Task.Run(() => Powersupple_AE800.Instance.SetVoltage(12));
3. 读写超时设置要合理
cs
serialPort.ReadTimeout = 2000; // 至少 1~2 秒,避免线程饿死
serialPort.WriteTimeout = 1000;
4. 避免"长事务"阻塞其他线程
-
如果一次发送/接收需要几百毫秒,
用
Task.Run(() => ae0.ReadVoltage())
包一层,
但锁仍留在单例内部,不会破坏线程安全。
简单模板
cs
public void SafeRead()
{
double v = 0;
ThreadPool.QueueUserWorkItem(_ =>
{
lock (Powersupple_AE0.Instance)
{
v = Powersupple_AE0.Instance.ReadVoltage();
}
BeginInvoke(() => lblVoltage.Text = $"{命令} V");
});
}
总结:加同一把锁 + 不要直接碰 SerialPort + 合理超时