1、什么是三次握手?什么是四次挥手?
一句话速记
三次握手 =建立 TCP 连接 ;四次挥手 =断开 TCP 连接。
一、三次握手(建连接)
-
第一次:客户端发 SYN → 问服务器:我能发消息不?
-
第二次:服务器回 SYN+ACK → 我收得到,我也能发,你能收到我吗?
-
第三次:客户端发 ACK → 我能收到你,连接就绪。
作用:确认双方收发都正常,避免旧无效请求占用资源。
二、四次挥手(断连接)
TCP 是全双工,两边通道要分别关闭,所以要 4 步:
-
第一次:主动方发 FIN → 我不发数据了。
-
第二次:被动方回 ACK → 收到,你不发我知道了(我还能继续发数据给你)。
-
第三次:被动方发 FIN → 我也发完了,我也不发了。
-
第四次:主动方回 ACK → 收到,彻底断开。
简单对比:建连接合并了确认,3 步搞定;断连接两边各自收尾,必须拆成 4 步。
2、C# 与 PLC 通讯(完整入门 + 实战代码)
C# 和 PLC 通讯是工业自动化最常用的开发场景,主流用 2 种协议:
-
Modbus(最通用、几乎所有 PLC 都支持)
-
S7 Protocol(西门子专用,速度最快)
我直接给你可复制运行的完整代码,不用你自己拼。
一、最推荐:C# 使用 Modbus 与 PLC 通讯
1. 安装库(NuGet)
Install-Package NModbus
或在 Visual Studio 里搜索安装 NModbus
2. C# 读写 PLC 完整代码(串口 / TCP 都支持)
TCP 方式(最常用)
using System;
using System.Net.Sockets;
using Modbus.Device;
class ModbusPLC
{
static void Main(string[] args)
{
try
{
// 1. 连接 PLC 的 IP 和 端口(Modbus TCP 默认 502)
TcpClient tcp = new TcpClient("192.168.2.1", 502);
ModbusIpMaster master = ModbusIpMaster.CreateIp(tcp);
// 2. 读取保持寄存器(PLC 最常用:D区 / 保持寄存器)
// 参数:从站号、起始地址、读取长度
ushort[] readData = master.ReadHoldingRegisters(1, 0, 10);
Console.WriteLine("读取成功:" + readData[0]);
// 3. 写入单个保持寄存器
master.WriteSingleRegister(1, 0, 666);
Console.WriteLine("写入成功");
tcp.Close();
}
catch (Exception ex)
{
Console.WriteLine("错误:" + ex.Message);
}
}
}
串口 RS485 / RS232 方式
using System.IO.Ports;
SerialPort port = new SerialPort("COM3", 9600, Parity.None, 8, StopBits.One);
port.Open();
ModbusSerialMaster master = ModbusSerialMaster.CreateRtu(port);
// 读写同上
二、西门子 S7 PLC 专用通讯(S7.NET)
如果你的 PLC 是 西门子 S7-200/300/400/1200/1500,用这个库最快。
1. 安装库
Install-Package S7.NET
2. 完整代码
using System;
using S7.Net;
class SiemensPLC
{
static void Main(string[] args)
{
// 选择 PLC 类型 + IP
Plc plc = new Plc(CpuType.S71200, "192.168.0.1, 0, 1");
// 打开连接
plc.Open();
// 读取 DB 块数据(最常用)
// 参数:DB块号, 起始地址, 数据类型
int value = plc.ReadInt("DB1.DBW0");
Console.WriteLine("读取 DB1.DBW0 = " + value);
// 写入数据
plc.Write("DB1.DBW0", 1234);
// 关闭
plc.Close();
}
}
三、你必须知道的关键知识点
1. 常用地址对应
-
Modbus 保持寄存器 → 对应 PLC D 区 / VW / RW
-
Modbus 线圈 → 对应 PLC Y / Q 输出
-
Modbus 输入寄存器 → 对应 PLC AI 模拟量
2. 通讯失败常见原因
-
PLC IP 和电脑不在同一网段
-
端口未开放(Modbus 502,西门子 102)
-
从站号(Slave ID)错误
-
地址格式写错
3. 工业开发最佳实践
-
用 异步读写 不卡顿界面
-
加 重连机制
-
加 心跳包 检测连接状态
四、我可以直接帮你
你告诉我 3 个信息,我就能给你完全匹配你设备的可运行代码:
-
PLC 品牌型号(西门子 / 三菱 / 欧姆龙 / 台达 / 信捷...)
-
通讯方式(网口 TCP / 串口 RS485)
-
要读写的地址(如 D0、DB1.DBW0、M0.0)
总结
-
Modbus(NModbus) → 通用所有 PLC
-
S7.NET → 西门子专用,速度最快
-
代码都是直接复制可运行的工业级标准写法
3、线程、线程池、Task
Thread 是原生底层线程,ThreadPool 是复用线程池省资源,Task 是高级封装(基于池 + 异步回调),现代 C# 优先用 Task/async await。
1. Thread 原生线程
最底层、最麻烦的手动线程。
- 特点:
-
手动创建销毁,开销大
-
无法返回结果、异常难捕获
-
生命周期全自己管
- 简单示例
Thread t = new Thread(()=>{
Console.WriteLine("原生线程执行");
});
t.Start();
缺点:创建多了会卡、难管理,项目里基本不用。
2. ThreadPool 线程池
提前建好一批线程,任务来了直接复用。
- 特点:
-
自动复用线程,减少创建开销
-
不能精细控制、没法获取返回值
-
后台线程,程序退出直接被杀
- 示例
ThreadPool.QueueUserWorkItem(state=>{
Console.WriteLine("线程池任务");
});
场景:老项目遗留用,新项目不推荐。
3. Task & async/await(现在主力)
基于线程池封装,功能最强、最优雅。
- 特点:
-
默认用线程池,性能好
-
支持返回值、异常捕获、链式延续
-
完美配合 async/await,UI 不卡顿
- 基础示例
// 无返回
Task.Run(()=>{
Console.WriteLine("Task执行");
});
// 有返回
async Task<int> GetNumAsync()
{
return await Task.Run(()=> 100);
}
三者对比速记
-
Thread:重型、手动控、淘汰不用
-
ThreadPool:复用线程、功能简陋、老代码用
-
Task:轻量强大、支持异步链式、工业上位机 / WPF 首选
上位机开发小建议
UI 界面永远别用 Thread 死循环;耗时读写 PLC / 相机全用async Task+await,界面永不卡死。
4、C# 工业上位机通用模板:Task 异步 + 取消令牌 + PLC 通信 + 异常处理
一句话总结
整套模板用Task+async/await+CancellationToken,安全异步读写 PLC,UI 永不卡死、随时取消、异常全覆盖,直接可落地 WPF/WinForm 项目。
1. 核心工具类(通用 PLC 异步操作封装)
using System;
using System.Threading;
using System.Threading.Tasks;
using Modbus.Device;
using System.Net.Sockets;
public class PlcAsyncHelper
{
private TcpClient _tcpClient;
private ModbusIpMaster _modbusMaster;
// 连接状态
public bool IsConnected => _tcpClient?.Connected ?? false;
#region 异步连接PLC
public async Task<bool> ConnectAsync(string ip, int port = 502, CancellationToken ct = default)
{
try
{
_tcpClient = new TcpClient();
// 异步连接,支持取消
await _tcpClient.ConnectAsync(ip, port, ct);
_modbusMaster = ModbusIpMaster.CreateIp(_tcpClient);
return true;
}
catch (OperationCanceledException)
{
Console.WriteLine("PLC连接已取消");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"连接异常:{ex.Message}");
Disconnect();
return false;
}
}
#endregion
#region 异步读取寄存器
public async Task<ushort[]> ReadRegistersAsync(byte slaveId, ushort startAddr, ushort length, CancellationToken ct = default)
{
if (!IsConnected) throw new InvalidOperationException("PLC未连接");
// 耗时操作包装Task,防止阻塞
return await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
return _modbusMaster.ReadHoldingRegisters(slaveId, startAddr, length);
}, ct);
}
#endregion
#region 异步写入寄存器
public async Task WriteRegisterAsync(byte slaveId, ushort addr, ushort value, CancellationToken ct = default)
{
if (!IsConnected) throw new InvalidOperationException("PLC未连接");
await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
_modbusMaster.WriteSingleRegister(slaveId, addr, value);
}, ct);
}
#endregion
#region 断开释放
public void Disconnect()
{
_tcpClient?.Close();
_tcpClient?.Dispose();
_modbusMaster = null;
}
#endregion
}
2. UI 层调用示例(WinForm/WPF 通用逻辑)
using System;
using System.Threading;
using System.Windows.Forms;
namespace PlcAsyncDemo
{
public partial class MainForm : Form
{
private readonly PlcAsyncHelper _plcHelper = new PlcAsyncHelper();
// 取消令牌源:控制所有异步任务停止
private CancellationTokenSource _cts;
public MainForm()
{
InitializeComponent();
}
// 连接按钮
private async void btnConnect_Click(object sender, EventArgs e)
{
_cts = new CancellationTokenSource();
bool res = await _plcHelper.ConnectAsync("192.168.1.10", 502, _cts.Token);
lblStatus.Text = res ? "PLC连接成功" : "PLC连接失败";
}
// 异步读取PLC按钮
private async void btnRead_Click(object sender, EventArgs e)
{
try
{
if (_cts == null) _cts = new CancellationTokenSource();
// 读取从站1,起始地址0,读取2个寄存器
var data = await _plcHelper.ReadRegistersAsync(1, 0, 2, _cts.Token);
txtResult.Text = $"读取值:{data[0]} , {data[1]}";
}
catch (OperationCanceledException)
{
txtResult.Text = "读取操作已取消";
}
catch (Exception ex)
{
txtResult.Text = $"读取报错:{ex.Message}";
}
}
// 停止所有任务按钮
private void btnStop_Click(object sender, EventArgs e)
{
// 触发取消
_cts?.Cancel();
_cts?.Dispose();
_cts = null;
}
// 窗口关闭释放资源
protected override void OnFormClosing(FormClosingEventArgs e)
{
_cts?.Cancel();
_cts?.Dispose();
_plcHelper.Disconnect();
base.OnFormClosing(e);
}
}
}
3. 关键知识点(适配上位机开发)
-
CancellationToken
随时终止异步读写,避免 PLC 卡死后台线程,关闭窗口必释放。
-
Task.Run + async/await
耗时 PLC 通信丢线程池,UI 主线程完全不阻塞,界面流畅。
-
异常分层捕获
单独处理取消异常、连接异常、读写异常,工业现场稳定不崩溃。
-
资源自动释放
断开、窗口关闭时清空连接和令牌,杜绝内存泄漏。
4. 扩展适配
-
西门子 PLC:把 Modbus 逻辑替换成
S7.NET异步方法,结构完全不变; -
循环轮询 PLC:加
while(!ct.IsCancellationRequested)做后台周期读取。
5、C# 工业上位机终极模板:Task 异步 + 取消令牌 + PLC 轮询 + 断线自动重连
整套直接能用,适配 WPF/WinForm,后台常驻轮询、掉线自动重试、全程不卡 UI。
一、封装 PLC 核心帮助类(Modbus TCP)
using System;
using System.Net.Sockets;
using System.Threading;
using System.Threading.Tasks;
using Modbus.Device;
public class PlcPollHelper
{
#region 基础变量
private TcpClient _tcpClient;
private ModbusIpMaster _master;
public string Ip { get; set; }
public int Port { get; set; } = 502;
public byte SlaveId { get; set; } = 1;
// 连接状态
public bool IsConnected => _tcpClient?.Connected ?? false;
// 轮询取消令牌
private CancellationTokenSource _pollCts;
private Task _pollTask;
// 轮询间隔ms
public int PollIntervalMs { get; set; } = 200;
#endregion
#region 连接&断开
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
try
{
Disconnect();
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(Ip, Port, ct);
_master = ModbusIpMaster.CreateIp(_tcpClient);
return true;
}
catch
{
Disconnect();
return false;
}
}
public void Disconnect()
{
StopPoll();
_tcpClient?.Close();
_tcpClient?.Dispose();
_master = null;
_tcpClient = null;
}
#endregion
#region 读写寄存器
public async Task<ushort[]> ReadRegAsync(ushort startAddr, ushort len, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (!IsConnected) throw new Exception("PLC未连接");
return await Task.Run(() =>
{
return _master.ReadHoldingRegisters(SlaveId, startAddr, len);
}, ct);
}
public async Task WriteRegAsync(ushort addr, ushort val, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (!IsConnected) throw new Exception("PLC未连接");
await Task.Run(() =>
{
_master.WriteSingleRegister(SlaveId, addr, val);
}, ct);
}
#endregion
#region 后台自动轮询 + 断线重连
// 开启轮询
public void StartPoll(Func<ushort[], Task> onDataReceived, Action<string> onLog)
{
if (_pollCts != null) return;
_pollCts = new CancellationTokenSource();
var token = _pollCts.Token;
_pollTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
// 没连接就自动重连
if (!IsConnected)
{
onLog?.Invoke("PLC离线,尝试重连...");
bool ok = await ConnectAsync(token);
if (ok)
onLog?.Invoke("PLC重连成功!");
else
{
await Task.Delay(1000, token); // 重连失败延时等待
continue;
}
}
// 正常读取:地址0,读取4个寄存器
var data = await ReadRegAsync(0, 4, token);
await onDataReceived?.Invoke(data);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
onLog?.Invoke($"轮询异常:{ex.Message},准备重连");
Disconnect();
await Task.Delay(1000, token);
}
// 轮询间隔
await Task.Delay(PollIntervalMs, token);
}
}, token);
}
// 停止轮询
public void StopPoll()
{
_pollCts?.Cancel();
_pollCts?.Dispose();
_pollCts = null;
}
#endregion
}
二、UI 调用示例(WinForm 通用)
using System;
using System.Windows.Forms;
using System.Threading.Tasks;
namespace PlcFullDemo
{
public partial class MainForm : Form
{
private readonly PlcPollHelper _plc = new PlcPollHelper();
public MainForm()
{
InitializeComponent();
// 初始化PLC参数
_plc.Ip = "192.168.1.10";
_plc.Port = 502;
_plc.SlaveId = 1;
_plc.PollIntervalMs = 200;
}
// 启动轮询按钮
private void btnStartPoll_Click(object sender, EventArgs e)
{
_plc.StartPoll(OnPlcDataUpdate, AddLog);
AddLog("已启动后台轮询");
}
// 停止轮询按钮
private void btnStopPoll_Click(object sender, EventArgs e)
{
_plc.Disconnect();
AddLog("已停止轮询 & 断开PLC");
}
// PLC数据回调
private async Task OnPlcDataUpdate(ushort[] data)
{
// 切回UI线程更新
Invoke(new Action(() =>
{
txtData.Text = $"D0:{data[0]} D1:{data[1]} D2:{data[2]} D3:{data[3]}";
}));
}
// 日志打印
private void AddLog(string msg)
{
Invoke(new Action(() =>
{
txtLog.AppendText($"{DateTime.Now:HH:mm:ss} {msg}\r\n");
}));
}
// 窗口关闭释放资源
protected override void OnFormClosing(FormClosingEventArgs e)
{
_plc.Disconnect();
base.OnFormClosing(e);
}
// 写入PLC按钮
private async void btnWrite_Click(object sender, EventArgs e)
{
try
{
await _plc.WriteRegAsync(10, 999, CancellationToken.None);
AddLog("写入D10=999成功");
}
catch (Exception ex)
{
AddLog($"写入失败:{ex.Message}");
}
}
}
}
三、核心亮点(工业项目刚需)
-
全程 async/Task:轮询跑后台,UI 永远不卡死
-
自动断线重连:网线拔掉、PLC 断电,恢复后自动连上
-
CancellationToken 管控:停止、关闭窗口立刻释放任务,无残留线程
-
异常全局捕获:单个读写报错不崩整个程序
-
UI 线程隔离:Invoke 回调更新界面,不会跨线程报错
四、西门子 S7 适配说明
只需替换读写底层:把 Modbus ReadHoldingRegisters 换成 S7.NET 的异步读写,轮询、重连、取消整套架构完全不用改。
6、C# 上位机|S7.NET 完整版:异步 Task + 自动轮询 + 断线重连 + 取消令牌
直接替换就能用,架构和刚才 Modbus 完全一致,适配西门子 S7-1200/1500/300/400。
1. 先安装 NuGet
Install-Package S7.NET
2. S7 专用 PLC 帮助类(核心封装)
using System;
using System.Threading;
using System.Threading.Tasks;
using S7.Net;
public class S7PlcHelper
{
#region 基础配置
private Plc _s7Plc;
// PLC参数
public string IpAddress { get; set; }
public CpuType CpuType { get; set; }
public int Rack { get; set; }
public int Slot { get; set; }
// 连接状态
public bool IsConnected => _s7Plc?.IsConnected ?? false;
// 轮询控制
private CancellationTokenSource _pollCts;
private Task _pollTask;
public int PollIntervalMs { get; set; } = 200;
#endregion
#region 连接 & 断开
public async Task<bool> ConnectAsync(CancellationToken ct = default)
{
try
{
Disconnect();
_s7Plc = new Plc(CpuType, IpAddress, Rack, Slot);
// 异步连接包装
await Task.Run(() =>
{
ct.ThrowIfCancellationRequested();
_s7Plc.Open();
}, ct);
return _s7Plc.IsConnected;
}
catch
{
Disconnect();
return false;
}
}
public void Disconnect()
{
StopPoll();
if (_s7Plc != null)
{
if (_s7Plc.IsConnected) _s7Plc.Close();
_s7Plc.Dispose();
_s7Plc = null;
}
}
#endregion
#region 常用读写封装
// 读DB块 Int16
public async Task<short> ReadDbIntAsync(int dbNum, int startOffset, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (!IsConnected) throw new Exception("PLC未连接");
return await Task.Run(() =>
{
return _s7Plc.ReadInt($"DB{dbNum}.DBW{startOffset}");
}, ct);
}
// 写DB块 Int16
public async Task WriteDbIntAsync(int dbNum, int startOffset, short value, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (!IsConnected) throw new Exception("PLC未连接");
await Task.Run(() =>
{
_s7Plc.Write($"DB{dbNum}.DBW{startOffset}", value);
}, ct);
}
// 批量读取DB多个Int
public async Task<short[]> ReadDbIntArrayAsync(int dbNum, int startOffset, int count, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
if (!IsConnected) throw new Exception("PLC未连接");
return await Task.Run(() =>
{
var res = new short[count];
for (int i = 0; i < count; i++)
{
res[i] = _s7Plc.ReadInt($"DB{dbNum}.DBW{startOffset + i * 2}");
}
return res;
}, ct);
}
#endregion
#region 后台自动轮询 + 断线重连
public void StartPoll(Func<short[], Task> dataCallback, Action<string> logCallback)
{
if (_pollCts != null) return;
_pollCts = new CancellationTokenSource();
var token = _pollCts.Token;
_pollTask = Task.Run(async () =>
{
while (!token.IsCancellationRequested)
{
try
{
// 断线自动重连
if (!IsConnected)
{
logCallback?.Invoke("西门子PLC离线,尝试重连...");
bool ok = await ConnectAsync(token);
if (ok)
logCallback?.Invoke("西门子PLC重连成功!");
else
{
await Task.Delay(1000, token);
continue;
}
}
// 读取DB1 从0开始4个Int
var data = await ReadDbIntArrayAsync(1, 0, 4, token);
await dataCallback?.Invoke(data);
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
logCallback?.Invoke($"轮询异常:{ex.Message},准备重连");
Disconnect();
await Task.Delay(1000, token);
}
await Task.Delay(PollIntervalMs, token);
}
}, token);
}
public void StopPoll()
{
_pollCts?.Cancel();
_pollCts?.Dispose();
_pollCts = null;
}
#endregion
}
3. WinForm UI 调用示例
using System;
using System.Windows.Forms;
namespace S7PlcDemo
{
public partial class MainForm : Form
{
private readonly S7PlcHelper _plc = new S7PlcHelper();
public MainForm()
{
InitializeComponent();
// 初始化S7参数
_plc.IpAddress = "192.168.0.1";
_plc.CpuType = CpuType.S71200;
_plc.Rack = 0;
_plc.Slot = 1;
_plc.PollIntervalMs = 200;
}
private void btnStartPoll_Click(object sender, EventArgs e)
{
_plc.StartPoll(OnDataUpdate, AddLog);
AddLog("已启动S7后台轮询");
}
private void btnStopPoll_Click(object sender, EventArgs e)
{
_plc.Disconnect();
AddLog("停止轮询并断开PLC");
}
// 数据回调更新UI
private async Task OnDataUpdate(short[] data)
{
Invoke(new Action(() =>
{
txtShow.Text = $"DB1.DBW0:{data[0]} DBW2:{data[1]} DBW4:{data[2]} DBW6:{data[3]}";
}));
}
// 日志输出
private void AddLog(string msg)
{
Invoke(new Action(() =>
{
txtLog.AppendText($"{DateTime.Now:HH:mm:ss} {msg}\r\n");
}));
}
// 写入测试按钮
private async void btnWrite_Click(object sender, EventArgs e)
{
try
{
await _plc.WriteDbIntAsync(1, 0, 1234, CancellationToken.None);
AddLog("写入 DB1.DBW0 = 1234 成功");
}
catch (Exception ex)
{
AddLog($"写入失败:{ex.Message}");
}
}
// 窗口关闭释放资源
protected override void OnFormClosing(FormClosingEventArgs e)
{
_plc.Disconnect();
base.OnFormClosing(e);
}
}
}
4. 快速配置说明
-
S7-1200/1500:
CpuType.S71200,Rack=0,Slot=1 -
S7-300/400:对应修改 CpuType 即可
-
PLC 需放开:PUT/GET 访问权限,取消 DB 块优化
5. 核心优势
-
全程
Task+async,UI 绝不卡顿 -
掉线自动重试重连,工业现场稳定
-
CancellationToken 统一管理退出,无内存泄漏
-
直接可用,不用再改架构
7、多线程中经常访问同一个资源可能造成什么问题
多线程同时抢同一个共享资源,核心问题就是数据错乱、逻辑崩坏、程序死锁崩溃。
1. 最常见:竞态条件(数据脏读 / 脏写)
多个线程同时读写同一个变量,计算被覆盖、结果不对。
举个极简例子:
private static int count = 0;
// 两个线程同时执行 count++
count++;
count++实际分三步:读→加→写。
线程互相插队,最后总数变少,数据不一致。
2. 线程安全异常(集合报错)
List、Dictionary 这类非线程安全集合:
多线程同时增删改,直接抛异常、数据丢包甚至内存损坏。
3. 死锁(程序卡死不动)
多个线程互相拿着对方需要的锁:
A 锁 1 等锁 2,B 锁 2 等锁 1 → 永久卡死,界面僵死。
4. 可见性问题(缓存没刷新)
一个线程改了变量,别的线程看不到最新值,一直用旧数据,逻辑卡死不走。
5. 重入异常 & 资源泄漏
连接、句柄被多线程重复释放 / 重复创建,直接崩程序、内存泄漏。
C# 上位机快速解决办法(你项目直接用)
-
简单变量:用
lock(obj)加互斥锁 -
原子计数:用
Interlocked.Increment -
集合:用
ConcurrentDictionary线程安全集合 -
异步 PLC/IO:用
SemaphoreSlim限流排队,防止并发抢通讯口
8、简单说明线程和进程之间的关系
进程是资源独立大房子,线程是屋里干活的人;一个进程至少有 1 个主线程,线程共享进程资源。
简单拆解
-
进程
操作系统资源分配最小单位,相互独立,崩溃互不影响。
-
线程
进程里的执行单元,调度 CPU 的最小单位;同进程线程共享内存、变量、文件句柄。
核心关系
-
1 进程 ≥ 1 线程
-
进程隔离,线程共享
-
进程开销大;线程轻量、切换快
接地气比喻
电脑软件 =房子 (进程)
代码执行流 =住户线程
房子各自独立;同住一间房的人共用家具家电。
9、单线程和多线程
单线程串行干活、简单不冲突;多线程并行干活、效率高但要控共享资源。
1. 单线程
同一时间只干一件事,从头到尾排队执行。
-
优点:代码简单、无资源冲突、不用加锁、不易 bug
-
缺点:一个耗时操作卡住,整个程序卡死
-
场景:简单小工具、UI 主线程
2. 多线程
同时开多条执行流,几件事一起跑。
-
优点:耗时任务后台跑,UI 不卡;多核 CPU 利用率高
-
缺点:共享变量会乱序、要加锁防冲突;容易死锁、逻辑复杂
-
场景:上位机轮询 PLC、读写相机、后台日志保存
3. 通俗对比
-
单线程:一个人做饭,洗菜→切菜→炒菜排队来,卡一步全停。
-
多线程:多人分工,一人洗菜、一人炒菜,效率高但抢工具要排队谦让。
上位机口诀
UI 界面用单线程不动后台;
通讯采集用多线程 Task放后台;
共享数据加锁,绝不裸奔读写。
1. 单线程版本(UI 直接卡死)
// 按钮点击事件,UI主线程执行
private void btnSingleThread_Click(object sender, EventArgs e)
{
txtResult.Text = "开始执行...";
// 耗时循环,霸占主线程
for (int i = 0; i < 500000000; i++)
{
}
txtResult.Text = "执行结束!";
}
现象:点击后窗口拖不动、按钮点不了、直接白屏卡死,等循环跑完才恢复。
2. 多线程 Task 版本(UI 完全不卡)
private async void btnMultiThread_Click(object sender, EventArgs e)
{
txtResult.Text = "后台开始执行...";
// 耗时任务丢线程池后台跑
await Task.Run(() =>
{
for (int i = 0; i < 500000000; i++)
{
}
});
txtResult.Text = "后台执行结束!";
}
现象:点击后界面流畅可操作,后台默默算,完事自动更新文本。
核心区别一句话
耗时操作写 UI 主线程 = 卡死;
耗时操作丢Task.Run后台 = 流畅不卡顿。
10、什么是线程锁
线程锁就是给共享资源装一把排队钥匙,同一时间只允许一个线程进去操作,防止数据乱掉。
1. 为什么需要锁
多线程同时读写同一个变量:
-
你改一半,我又来改
-
最后数据错乱、结果错误
2. 线程锁最常用:lock
// 定义锁对象
private readonly object _lockObj = new object();
private int count = 0;
void SafeAdd()
{
lock (_lockObj)
{
// 这一段代码同一时间只能一个线程进来
count++;
}
}
3. 通俗理解
-
lock里面的代码 =单人卫生间 -
线程进去关门上锁,别人只能外面排队
-
用完开门走人,下一个再进
4. 简单优缺点
✅ 优点:解决竞态问题,数据安全不乱
❌ 缺点:
-
加太多会变慢、阻塞
-
乱用多个锁容易死锁卡死
上位机小口诀
共享变量读写必加锁;
通讯串口 / PLC 读写用一把锁排队,避免并发报错。
11、怎么给线程加锁
用 lock 锁一个私有只读对象,把读写共享变量的代码包起来,同一时间只允许一个线程执行。
1. 标准写法(工业项目最常用)
第一步:定义锁对象(只写 1 次)
// 专门用来上锁,不要改它
private readonly object _locker = new object();
private int _count = 0; // 共享资源
第二步:读写共享资源时加锁
public void SafeAdd()
{
// 进门上锁,出门自动解锁
lock (_locker)
{
_count++; // 这里绝对不会多线程乱抢
}
}
2. 对比:不加锁会错,加锁就正常
❌ 不加锁(多线程跑,数值算错)
void BadCode()
{
_count++; // 多线程同时改,结果偏小
}
✅ 加锁安全版
void GoodCode()
{
lock(_locker)
{
_count++;
}
}
3. 上位机通讯专用锁(重点!)
PLC / 串口同一时间只能一个任务读写,必须加锁排队:
private readonly object _plcLock = new object();
public void ReadPlcData()
{
lock(_plcLock)
{
// 任何PLC读写操作
plc.ReadMemory(...);
}
}
作用:防止多处同时读写 PLC,报错、断线、乱数据。
4. 两个必记规则
-
同一个共享资源,必须用同一把锁
-
锁里面代码尽量短,别写耗时延迟,否则卡顿死锁
12、线程的五个状态
线程 5 大状态:新建→就绪→运行→阻塞→终止,循环流转。
1. 五个状态通俗讲解
-
新建 New
刚 new 出来线程对象,还没 Start,没进入系统调度。
-
就绪 Runnable
已 Start,资源备好,等着 CPU 有空就立刻执行。
-
运行 Running
抢到 CPU 时间片,代码正在全速跑。
-
阻塞 Blocked/Waiting
被卡住暂停:
-
Sleep休眠 -
lock抢不到锁 -
等待 IO/PLC 通信
CPU 不再分配时间片,唤醒后回到
就绪
5.终止 Terminated
代码跑完 / 异常结束,线程彻底销毁,不能重启。
流转口诀
新建就绪抢 CPU,运行遇阻变阻塞;
唤醒重回就绪队,跑完进入终止态。
C# 对应场景速记
-
New:
Thread t = new Thread(...) -
Runnable:
t.Start() -
Running:线程正在执行循环 / 业务
-
Blocked:
Thread.Sleep、等待 lock、await 延时 -
Terminated:方法执行完毕
线程五状态流转极简图(超好记)
【新建 New】
↓ Start()
【就绪 Runnable】 ←──────┐
↓ 抢到CPU时间片 │唤醒/等待结束
【运行 Running】 │
↙ ↘
阻塞等待 代码执行完毕
【阻塞 Blocked】 【终止 Terminated】
快速流转口诀
-
新建线程 →调用 Start→ 进入就绪排队
-
就绪抢到 CPU →变成运行
-
运行时 Sleep / 等锁 / IO →切到阻塞
-
阻塞结束唤醒 →回到就绪
-
代码跑完 / 异常退出 →进入终止永久结束
C# 对应场景秒懂
-
New:
new Thread(xxx)只创建不启动 -
Runnable:调用
.Start()等待调度 -
Running:线程正在执行业务代码
-
Blocked:
Sleep、抢 lock 失败、await 延迟 -
Terminated:方法执行结束,线程销毁
13、服务器端接收客户消息并输出代码
C# TCP 服务端:接收客户端消息并控制台输出(最简可直接运行)
1. 控制台 TCP 服务器代码
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class TcpServerDemo
{
// 监听IP + 端口
private const string ListenIp = "127.0.0.1";
private const int Port = 8888;
static async Task Main(string[] args)
{
// 创建监听器
var ipEndPoint = new IPEndPoint(IPAddress.Parse(ListenIp), Port);
TcpListener listener = new TcpListener(ipEndPoint);
try
{
listener.Start();
Console.WriteLine($"服务器已启动,监听 {ListenIp}:{Port}");
// 循环等待客户端连接
while (true)
{
// 异步等待客户端接入
TcpClient client = await listener.AcceptTcpClientAsync();
Console.WriteLine("新客户端已连接!");
// 开启独立任务处理当前客户端消息(不阻塞新连接)
_ = HandleClientAsync(client);
}
}
catch (Exception ex)
{
Console.WriteLine("服务器异常:" + ex.Message);
}
}
// 处理单个客户端收发
private static async Task HandleClientAsync(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
{
byte[] buffer = new byte[1024 * 4];
while (true)
{
try
{
// 读取客户端发来的数据
int readLen = await stream.ReadAsync(buffer, 0, buffer.Length);
if (readLen <= 0) break; // 客户端断开
// 转字符串并控制台输出
string msg = Encoding.UTF8.GetString(buffer, 0, readLen);
Console.WriteLine($"收到客户端消息:{msg}");
}
catch
{
break;
}
}
Console.WriteLine("客户端已断开连接");
}
}
}
2. 配套极简客户端(测试用)
using System;
using System.Net.Sockets;
using System.Text;
class TcpClientDemo
{
static void Main(string[] args)
{
TcpClient client = new TcpClient();
client.Connect("127.0.0.1", 8888);
var stream = client.GetStream();
Console.WriteLine("请输入要发送的消息:");
string input = Console.ReadLine();
byte[] data = Encoding.UTF8.GetBytes(input);
stream.Write(data, 0, data.Length);
Console.WriteLine("发送完成!");
Console.ReadKey();
}
}
使用步骤
-
先运行服务器,开启监听
-
再运行客户端,输入文字发送
-
服务器控制台自动打印收到的消息
关键特点
-
异步
Task架构,多客户端同时连接不卡顿 -
自动释放 Socket 资源,断开自动回收
-
UTF8 编码通用,工业 / 普通通信都能用
一、完整版 TCP 异步服务端(不变,支持多客户端长连接)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
class TcpLongServer
{
const int Port = 8888;
static async Task Main()
{
var listener = new TcpListener(IPAddress.Any, Port);
listener.Start();
Console.WriteLine($"服务端启动,监听端口 {Port}\n");
while (true)
{
var client = await listener.AcceptTcpClientAsync();
Console.WriteLine("新客户端接入");
// 后台独立处理长连接
_ = HandleClientLoop(client);
}
}
private static async Task HandleClientLoop(TcpClient client)
{
using (client)
using (var stream = client.GetStream())
{
byte[] buffer = new byte[1024];
while (true)
{
try
{
int len = await stream.ReadAsync(buffer, 0, buffer.Length);
if (len <= 0) break;
// 收到消息直接打印
string msg = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine($"收到客户端:{msg}");
}
catch
{
break;
}
}
}
Console.WriteLine("客户端断开连接\n");
}
}
二、长连接客户端(可无限输入、一直发消息不断开)
using System;
using System.Net.Sockets;
using System.Text;
class TcpLongClient
{
static void Main()
{
try
{
TcpClient client = new TcpClient();
Console.WriteLine("正在连接服务器...");
client.Connect("127.0.0.1", 8888);
Console.WriteLine("连接成功!随时输入消息发送,exit退出\n");
var stream = client.GetStream();
while (true)
{
Console.Write("请输入消息:");
string input = Console.ReadLine();
// 输入 exit 关闭客户端
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
break;
byte[] data = Encoding.UTF8.GetBytes(input);
stream.Write(data, 0, data.Length);
Console.WriteLine("发送完成\n");
}
client.Close();
}
catch (Exception ex)
{
Console.WriteLine("连接异常:" + ex.Message);
}
}
}
使用步骤
-
先运行服务端控制台
-
再运行客户端控制台
-
客户端随便打字回车发送,服务端实时打印
-
输入
exit关闭客户端
特点总结
-
服务端:多客户端同时接入、异步长连接、自动回收
-
客户端:长连接不掉线,循环输入无限发消息
TCP 双向通信完整版:服务端收消息 + 自动回复 + 客户端长连接连发
1. 服务端(多客户端异步 + 接收打印 + 自动回信)
using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
namespace TcpServerTwoWay
{
class Program
{
private const int Port = 8888;
static async Task Main(string[] args)
{
var listener = new TcpListener(IPAddress.Any, Port);
listener.Start();
Console.WriteLine($"双向通信服务端启动,监听端口 {Port}\n");
// 循环等待多个客户端连接
while (true)
{
var client = await listener.AcceptTcpClientAsync();
Console.WriteLine("✅ 新客户端已连接");
_ = HandleClientAsync(client);
}
}
// 单独处理每个客户端:收消息+自动回复
private static async Task HandleClientAsync(TcpClient client)
{
using (client)
using (NetworkStream stream = client.GetStream())
{
byte[] buffer = new byte[1024];
while (true)
{
try
{
// 读取客户端消息
int readLen = await stream.ReadAsync(buffer, 0, buffer.Length);
if (readLen <= 0) break;
// 解码打印
string recMsg = Encoding.UTF8.GetString(buffer, 0, readLen);
Console.WriteLine($"📥 收到客户端:{recMsg}");
// 组装回复消息
string reply = $"服务端已收到:{recMsg}";
byte[] sendData = Encoding.UTF8.GetBytes(reply);
await stream.WriteAsync(sendData, 0, sendData.Length);
Console.WriteLine($"📤 已自动回复客户端\n");
}
catch
{
break;
}
}
}
Console.WriteLine("❌ 客户端断开连接\n");
}
}
}
2. 客户端(长连接 + 循环发消息 + 接收服务端回信)
using System;
using System.Net.Sockets;
using System.Text;
namespace TcpClientTwoWay
{
class Program
{
static void Main(string[] args)
{
try
{
TcpClient client = new TcpClient();
Console.WriteLine("正在连接服务端 127.0.0.1:8888...");
client.Connect("127.0.0.1", 8888);
Console.WriteLine("✅ 连接成功!输入消息发送,exit退出\n");
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
while (true)
{
Console.Write("请输入发送内容:");
string input = Console.ReadLine();
// 退出指令
if (input.Equals("exit", StringComparison.OrdinalIgnoreCase))
break;
// 发送消息给服务端
byte[] sendBytes = Encoding.UTF8.GetBytes(input);
stream.Write(sendBytes, 0, sendBytes.Length);
// 等待并读取服务端回复
int len = stream.Read(buffer, 0, buffer.Length);
string recReply = Encoding.UTF8.GetString(buffer, 0, len);
Console.WriteLine($"💬 服务端回复:{recReply}\n");
}
client.Close();
Console.WriteLine("连接已关闭");
}
catch (Exception ex)
{
Console.WriteLine($"异常:{ex.Message}");
}
}
}
}
运行流程
-
先启动服务端控制台监听端口
-
再启动客户端自动连上服务端
-
客户端输入文字发送
-
服务端打印消息并自动回信
-
客户端收到回信并显示
-
输入
exit即可退出客户端
核心亮点
-
支持多个客户端同时连接互不干扰
-
全程长连接不断开,双向收发
-
异常自动断开回收资源,稳定好用
贴合你上位机网络通信的开发场景,直接复用即可。
14、什么是线程池
线程池就是系统提前建好一堆备用线程,任务来了直接拿现成的用,用完不销毁、放回池里复用,省资源、启动快。
1. 为什么不用手动 new Thread
-
每次新建线程:开销大、慢、难管理
-
线程太多:CPU 爆满、内存泄漏
2. 线程池核心特点
-
提前创建线程缓存,待命等待任务
-
自动复用线程:任务做完线程不删,等下一个任务
-
自动控数量:防止线程爆炸拖垮程序
-
.NET 里
ThreadPool/Task默认都跑在线程池
3. 通俗比喻
-
new Thread= 每次干活现招人,干完立刻辞退,费钱费力 -
线程池 = 常备一支固定小队,来活就上岗,没活就待命,永远不裁员
4. 最简代码演示
// 往线程池丢任务
ThreadPool.QueueUserWorkItem(state =>
{
Console.WriteLine("我是线程池里跑的任务");
});
5. 和你之前知识关联
-
Thread:原生重型线程,少用 -
ThreadPool:底层池,老旧写法 -
Task/async await:现代封装,底层默认就是线程池,上位机优先用这个
Thread / 线程池 / Task 对比(面试极简背诵版)
一句话总览
Thread = 原生重型手动线程;ThreadPool = 简易复用池子;Task = 高级封装(基于线程池 + 异步全能)。
对比表格
| 对比项 | Thread 原生线程 | ThreadPool 线程池 | Task (.NET 现代) |
|---|---|---|---|
| 底层来源 | 操作系统新建线程 | 预先创建一批线程复用 | 默认基于线程池封装 |
| 开销 | 很大,频繁创建销毁卡性能 | 小,线程复用不销毁 | 最轻量,智能调度 |
| 控制难度 | 复杂:启停、挂起全手动 | 几乎不能精细控制 | 灵活:取消、等待、回调齐全 |
| 返回结果 | 不支持 | 不支持 | 完美支持返回值 |
| 异常处理 | 极易崩主程序 | 难捕获 | 自带友好异常捕获 |
| 取消功能 | 无 | 无 | 支持 CancellationToken 取消 |
| 适用场景 | 老旧遗留项目,几乎淘汰 | 老代码维护 | 新项目 / 上位机 / 工业开发首选 |
核心口诀
-
Thread:重量级,自己造线程,开销大,别用。
-
ThreadPool:池子复用线程,功能简陋,现在少用。
-
Task:线程池升级版,异步强、能取消、能返回,开发全靠它。
和上位机开发关联
-
PLC 轮询、相机采集、网络通信:全部用 async + Task
-
不用自己 new Thread,不用手写 ThreadPool
-
自动复用线程池资源,界面不卡、稳定不崩
15、线程池的工作流程
线程池流程:预先建好线程待命 → 任务入队排队 → 空闲线程认领执行 → 用完线程回收待命,不销毁。
完整工作流程(5 步背会)
-
初始化预热
程序启动时,线程池提前创建一批空闲线程,静静待命。
-
接收任务排队
新业务任务进来,不新建线程,先放进任务队列排队。
-
空闲线程调度
池里有空闲线程 → 立刻取出任务执行;
无空闲且没到上限 → 自动少量新增线程;
线程已满负荷 → 任务排队等待。
-
执行任务
线程拿到业务逻辑,独立运行代码。
-
执行完回收复用
任务结束,线程不销毁,回归池子变回空闲,等待下一个任务复用。
极简流程图
初始化创建空闲线程
↓
新任务 → 进入队列
↓
空闲线程取出任务执行
↓
任务结束 → 线程回归池子待命(循环复用)
补充关键点(面试必考)
-
限制最大线程数,防止线程爆炸卡死 CPU
-
短任务适合线程池;超长死循环任务不建议用
-
C# 里
Task.Run底层自动走这套线程池流程,不用自己管理
16、IP类
C# IP 相关常用类 快速总结(面试 + 开发够用)
一、核心 3 个类
-
IPAddress :封装单个 IP 地址(如
127.0.0.1) -
IPEndPoint:IP + 端口 组合(网络通信必备)
-
Dns:域名解析、获取本机 IP
二、用法极简示例
1. IPAddress 使用
// 字符串转IP
IPAddress ip = IPAddress.Parse("192.168.1.10");
// 监听所有网卡
IPAddress anyIp = IPAddress.Any;
2. IPEndPoint(通信最常用)
// IP + 端口绑定
IPAddress ip = IPAddress.Parse("127.0.0.1");
IPEndPoint endPoint = new IPEndPoint(ip, 8888);
👉 TCP 服务器、PLC 连接、Socket 全靠它。
3. Dns 域名解析
// 域名转IP
IPHostEntry host = Dns.GetHostEntry("www.baidu.com");
foreach(var item in host.AddressList)
{
Console.WriteLine(item);
}
三、面试 / 开发常考点
-
IPAddress.Any= 监听本机所有网卡 IP -
127.0.0.1= 本地回环,只能自己访问 -
IPEndPoint是 socket 通信必须的终结点 -
本机多网卡时,绑定指定 IP 更稳定
C# IP 工具类:校验 IP + 获取本机所有 IP(直接能用)
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
public static class IpHelper
{
/// <summary>
/// 判断字符串是否是合法IPv4
/// </summary>
public static bool IsValidIPv4(string ipStr)
{
if (string.IsNullOrWhiteSpace(ipStr))
return false;
return IPAddress.TryParse(ipStr, out var ip)
&& ip.AddressFamily == AddressFamily.InterNetwork;
}
/// <summary>
/// 获取本机所有可用IPv4地址(排除回环)
/// </summary>
public static List<string> GetLocalAllIPv4()
{
List<string> ipList = new List<string>();
foreach (var ni in NetworkInterface.GetAllNetworkInterfaces())
{
// 跳过禁用/断开网卡
if (ni.OperationalStatus != OperationalStatus.Up) continue;
var addrs = ni.GetIPProperties().UnicastAddresses;
foreach (var addr in addrs)
{
// 只取IPv4、排除127.0.0.1
if (addr.Address.AddressFamily == AddressFamily.InterNetwork
&& !IPAddress.IsLoopback(addr.Address))
{
ipList.Add(addr.Address.ToString());
}
}
}
return ipList.Distinct().ToList();
}
/// <summary>
/// 域名解析成IP
/// </summary>
public static List<IPAddress> DnsResolve(string hostName)
{
try
{
var host = Dns.GetHostEntry(hostName);
return host.AddressList
.Where(x => x.AddressFamily == AddressFamily.InterNetwork)
.ToList();
}
catch
{
return new List<IPAddress>();
}
}
}
调用示例
// 1.校验IP
Console.WriteLine(IpHelper.IsValidIPv4("192.168.1.10")); // True
Console.WriteLine(IpHelper.IsValidIPv4("999.1.1.1")); // False
// 2.获取本机所有网卡IP
var localIps = IpHelper.GetLocalAllIPv4();
foreach(var ip in localIps)
{
Console.WriteLine("本机IP:" + ip);
}
// 3.域名解析
var dnsIps = IpHelper.DnsResolve("www.baidu.com");
高频常量速记
-
IPAddress.Any:监听本机所有网卡 IP(服务器常用) -
IPAddress.Loopback:127.0.0.1 本地回环 -
AddressFamily.InterNetwork:代表 IPv4
17、什么是IP地址,有什么作用?
IP 地址就是网络里每一台设备的唯一门牌号,用来找到对方、互相通信。
1. 什么是 IP 地址
-
全称:互联网协议地址
-
格式(IPv4):四段数字,
192.168.1.10 -
每台电脑 / PLC / 相机 / 服务器,联网后都有专属 IP
2. 核心作用
-
定位设备
局域网里靠 IP 区分:哪台是 PLC、哪台是上位机、哪台是相机。
-
路由转发数据
数据按 IP 寻址,准确发到目标设备,不会发错。
-
通信连接必备
TCP/PLC/ 网络通信,必须知道对方 IP + 端口才能连上。
3. 常见特殊 IP
-
127.0.0.1:本机回环,自己连自己测试用 -
192.168.x.x/10.x.x.x:内网私有 IP,工厂局域网常用 -
IPAddress.Any:监听本机所有网卡 IP
4. 通俗比喻
房子 = 网络设备
IP 地址 = 详细住址
没有地址,快递(网络数据)就送不到人手里。
18、什么是子网掩码,有什么作用?
子网掩码用来区分 IP 里哪部分是网段、哪部分是设备号,判断两台设备是不是在同一个局域网。
1. 简单理解
IPv4 是 IP地址 + 子网掩码 成对用。
常见掩码:255.255.255.0
2. 核心作用
-
划分网络位 & 主机位
IP:192.168.1.10
掩码:255.255.255.0
前面一样的是局域网网段 ,后面变化的是设备编号。
-
判断是否同网段(能不能直连通信)
两台设备:
-
IP & 子网掩码 做运算 → 网络地址一样 = 同局域网,直接互通
-
不一样 = 跨网段,要经过路由器转发
3. 通俗比喻
IP = 完整住址:小区 + 楼栋 + 房间号
子网掩码 = 划分规则:规定「小区楼栋」是公共网段,「房间号」是单独设备。
4. 工控常用口诀
工控 PLC / 上位机默认:
IP:192.168.1.X
掩码:255.255.255.0
只要前 3 段一样,就是同一局域网,直接通信没问题。