<think>我们选择WinForms + Task.Run来实现一个简单的工业设备监控示例。该示例模拟控制10台设备,每台设备独立线程(实际由线程池管理)进行数据采集,并在UI上实时更新状态。
### 示例工程说明
1. **功能**:
- 启动时创建10个虚拟设备(ID为1-10)。
- 每个设备在后台循环采集数据(模拟随机温度值)。
- 数据实时显示在DataGridView中,包括设备ID、温度、状态和最后更新时间。
- 可停止所有设备采集任务。
2. **核心机制**:
- 使用 `Task.Run` 为每个设备启动独立异步任务。
- 通过 `CancellationToken` 实现任务取消。
- UI更新使用 `Control.Invoke` 确保线程安全。
3. **项目结构**:
- `Device` 类:封装设备属性和采集逻辑。
- `MainForm`:主窗体,包含启动/停止按钮和数据显示控件。
---
### 完整代码(含详细注释)
```csharp
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace IndustrialDeviceMonitor
{
public partial class MainForm : Form
{
// 设备列表
private List<Device> devices = new List<Device>();
// 设备任务列表,用于管理后台任务
private List<Task> deviceTasks = new List<Task>();
// 取消令牌源,用于取消所有任务
private CancellationTokenSource cts = new CancellationTokenSource();
public MainForm()
{
InitializeComponent();
SetupDataGridView();
InitializeDevices();
}
// 初始化DataGridView列
private void SetupDataGridView()
{
dataGridView1.Columns.Add("DeviceID", "设备ID");
dataGridView1.Columns.Add("Temperature", "温度(℃)");
dataGridView1.Columns.Add("Status", "状态");
dataGridView1.Columns.Add("LastUpdate", "最后更新时间");
dataGridView1.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
}
// 初始化10个设备
private void InitializeDevices()
{
for (int i = 1; i <= 10; i++)
{
devices.Add(new Device(i));
// 添加一行空数据占位
dataGridView1.Rows.Add(i.ToString(), "", "未启动", "");
}
}
// 启动所有设备任务
private async void btnStart_Click(object sender, EventArgs e)
{
cts = new CancellationTokenSource(); // 重置取消令牌
deviceTasks.Clear();
// 为每个设备启动一个后台任务
foreach (var device in devices)
{
// 使用Task.Run启动任务,传入取消令牌
var task = Task.Run(() => device.StartMonitoring(UpdateUI, cts.Token), cts.Token);
deviceTasks.Add(task);
}
btnStart.Enabled = false;
btnStop.Enabled = true;
try
{
// 等待所有任务完成(实际会一直运行直到取消)
await Task.WhenAll(deviceTasks);
}
catch (OperationCanceledException)
{
// 任务被取消是预期行为,无需处理
}
}
// 停止所有设备任务
private void btnStop_Click(object sender, EventArgs e)
{
cts.Cancel(); // 触发取消
btnStop.Enabled = false;
btnStart.Enabled = true;
}
// 更新UI的回调方法(由设备线程调用)
private void UpdateUI(Device device)
{
// 如果当前不在UI线程,则通过Invoke切换
if (InvokeRequired)
{
Invoke(new Action<Device>(UpdateUI), device);
return;
}
// 找到对应设备ID的行(索引为ID-1)
int rowIndex = device.Id - 1;
dataGridView1.Rows[rowIndex].Cells["Temperature"].Value = device.Temperature;
dataGridView1.Rows[rowIndex].Cells["Status"].Value = device.IsMonitoring ? "运行中" : "已停止";
dataGridView1.Rows[rowIndex].Cells["LastUpdate"].Value = device.LastUpdateTime.ToString("HH:mm:ss.fff");
}
}
// 设备类
public class Device
{
public int Id { get; }
public double Temperature { get; private set; }
public bool IsMonitoring { get; private set; }
public DateTime LastUpdateTime { get; private set; }
private Random random = new Random();
public Device(int id)
{
Id = id;
}
// 设备监控主循环
public void StartMonitoring(Action<Device> updateCallback, CancellationToken token)
{
IsMonitoring = true;
try
{
while (!token.IsCancellationRequested)
{
// 模拟采集数据(随机温度值)
Temperature = 20 + random.NextDouble() * 15; // 20~35℃
LastUpdateTime = DateTime.Now;
// 调用回调更新UI
updateCallback?.Invoke(this);
// 模拟采集间隔(500ms)
Thread.Sleep(500);
}
}
finally
{
IsMonitoring = false;
updateCallback?.Invoke(this); // 更新状态为停止
}
}
}
}
```
---
### 关键原理详解
1. **`Task.Run` 工作流程**:
- 当点击启动按钮时,为每个设备调用 `Task.Run(() => device.StartMonitoring(...))`。
- `Task.Run` 将 `StartMonitoring` 方法放入线程池队列,由线程池分配空闲线程执行。
- 线程池自动管理线程数量:当任务阻塞(如`Thread.Sleep`)时,线程可能被回收复用。
2. **异步与取消**:
- `CancellationToken` 通过 `cts.Cancel()` 通知所有任务停止。
- 任务内部循环检查 `token.IsCancellationRequested` 实现优雅退出。
3. **UI更新安全**:
- 设备线程通过 `InvokeRequired` 检测当前线程是否非UI线程。
- 若在后台线程,则通过 `Invoke` 将更新操作委托给UI线程执行。
4. **线程池优势**:
- 10个设备仅需少量线程(如2-4个),因为 `Thread.Sleep(500)` 期间线程可执行其他设备任务。
- 对比直接创建10个 `Thread` 对象,减少资源消耗。
---
### 扩展建议
- **设备通信替换**:将 `Thread.Sleep(500)` 替换为实际设备读取(如 `SerialPort.Read` 或网络请求),并改用异步API避免阻塞。
- **错误处理**:在 `StartMonitoring` 中添加 `try-catch` 捕获设备异常。
- **性能监控**:添加 `ThreadPool.GetAvailableThreads` 日志以观察线程池状态。
§§相关问题§§
1. 在设备监控循环中,使用 `Thread.Sleep` 会阻塞线程池线程,如何改造为真正的异步等待?
2. 如果某个设备通信耗时很长(如1秒以上),如何避免线程池线程被耗尽?
3. 如何修改此示例,使得设备数据更新频率可动态调整(如通过UI输入间隔时间)?
4. 在 `UpdateUI` 方法中频繁调用 `Invoke` 是否会导致性能问题?如何优化高频更新的UI渲染?
5. 如果在设备任务中发生未处理异常,程序会崩溃吗?如何安全捕获并显示异常信息?
</think>
### WinForms + Task.Run 完整示例工程详解
以下是一个完整的工业设备监控软件示例,使用 WinForms + `Task.Run` 实现多设备并发控制。包含详细中文注释,模拟 5 台设备的数据采集和控制流程。
#### 工程结构
```bash
IndustrialMonitor/
├── MainForm.cs # 主界面
├── DeviceManager.cs # 设备管理核心
├── Device.cs # 设备基类
├── EthernetDevice.cs # 以太网设备实现
├── SerialDevice.cs # 串口设备实现
└── Program.cs # 入口文件
```
---
### 1. 设备基类 (`Device.cs`)
```csharp
using System;
public abstract class Device
{
public int DeviceId { get; } // 设备唯一ID
public string Status { get; protected set; } = "离线"; // 设备状态
public event Action<Device> DataUpdated; // 数据更新事件
protected Device(int id) => DeviceId = id;
// 抽象方法:启动设备监控
public abstract Task StartMonitoringAsync(CancellationToken token);
// 触发UI更新
protected void NotifyDataUpdated() => DataUpdated?.Invoke(this);
}
```
### 2. 以太网设备实现 (`EthernetDevice.cs`)
```csharp
public class EthernetDevice : Device
{
private readonly string _ipAddress;
public decimal Temperature { get; private set; } // 模拟温度数据
public EthernetDevice(int id, string ip) : base(id)
{
_ipAddress = ip;
}
public override async Task StartMonitoringAsync(CancellationToken token)
{
Status = "运行中";
NotifyDataUpdated();
try
{
while (!token.isCancellationRequested)
{
// 模拟以太网通信:从设备读取数据
await Task.Delay(300, token); // 模拟300ms网络延迟
// 生成模拟数据 (20-40℃)
Temperature = 20m + (decimal)(new Random().NextDouble() * 20);
NotifyDataUpdated(); // 触发UI更新
await Task.Delay(1000, token); // 每秒采集一次
}
}
finally
{
Status = "已停止";
NotifyDataUpdated();
}
}
}
```
### 3. 串口设备实现 (`SerialDevice.cs`)
```csharp
public class SerialDevice : Device
{
private readonly string _comPort;
public int RPM { get; private set; } // 模拟转速数据
public SerialDevice(int id, string port) : base(id)
{
_comPort = port;
}
public override async Task StartMonitoringAsync(CancellationToken token)
{
Status = "运行中";
NotifyDataUpdated();
try
{
// 模拟串口初始化
await Task.Delay(500, token);
while (!token.isCancellationRequested)
{
// 模拟串口数据读取
await Task.Delay(150, token); // 模拟串口延迟
RPM = new Random().Next(800, 2000); // 随机转速800-2000
NotifyDataUpdated();
await Task.Delay(800, token); // 每0.8秒采集一次
}
}
finally
{
Status = "已停止";
NotifyDataUpdated();
}
}
}
```
---
### 4. 设备管理器 (`DeviceManager.cs`)
```csharp
using System.Collections.Concurrent;
public class DeviceManager
{
private readonly ConcurrentDictionary<int, Device> _devices = new();
private readonly List<Task> _monitorTasks = new();
private CancellationTokenSource _cts = new();
// 添加设备
public void AddDevice(Device device)
{
_devices.TryAdd(device.DeviceId, device);
device.DataUpdated += OnDeviceDataUpdated;
}
// 启动所有设备监控
public void StartAll()
{
_cts = new CancellationTokenSource();
foreach (var device in _devices.Values)
{
// 使用Task.Run启动设备监控任务
_monitorTasks.Add(Task.Run(() =>
device.StartMonitoringAsync(_cts.Token)
));
}
}
// 停止所有设备
public async Task StopAllAsync()
{
_cts.Cancel(); // 发送取消信号
await Task.WhenAll(_monitorTasks); // 等待所有任务安全退出
_monitorTasks.Clear();
}
// 设备更新回调(线程安全)
private void OnDeviceDataUpdated(Device device)
{
// UI更新将通过主窗体的Invoke处理
DeviceUpdated?.Invoke(device);
}
public event Action<Device> DeviceUpdated;
}
```
---
### 5. 主界面 (`MainForm.cs`)
```csharp
public partial class MainForm : Form
{
private readonly DeviceManager _deviceManager = new();
private readonly DataGridView _grid = new();
public MainForm()
{
InitializeComponent();
SetupUI();
CreateDevices();
}
// 初始化设备(3台以太网 + 2台串口)
private void CreateDevices()
{
_deviceManager.AddDevice(new EthernetDevice(1, "192.168.1.101"));
_deviceManager.AddDevice(new EthernetDevice(2, "192.168.1.102"));
_deviceManager.AddDevice(new EthernetDevice(3, "192.168.1.103"));
_deviceManager.AddDevice(new SerialDevice(4, "COM3"));
_deviceManager.AddDevice(new SerialDevice(5, "COM4"));
_deviceManager.DeviceUpdated += UpdateDeviceUI;
}
// 初始化UI
private void SetupUI()
{
// 数据表格设置
_grid.Dock = DockStyle.Fill;
_grid.Columns.AddRange(new[]
{
new DataGridViewTextBoxColumn { Name = "ID", HeaderText = "设备ID" },
new DataGridViewTextBoxColumn { Name = "Type", HeaderText = "类型" },
new DataGridViewTextBoxColumn { Name = "Status", HeaderText = "状态" },
new DataGridViewTextBoxColumn { Name = "Data", HeaderText = "实时数据" }
});
Controls.Add(_grid);
// 控制按钮
var btnStart = new Button { Text = "启动监控", Top = 450 };
var btnStop = new Button { Text = "停止监控", Top = 450, Left = 100 };
btnStart.Click += (s, e) => _deviceManager.StartAll();
btnStop.Click += async (s, e) => await _deviceManager.StopAllAsync();
Controls.Add(btnStart);
Controls.Add(btnStop);
}
// 线程安全的UI更新
private void UpdateDeviceUI(Device device)
{
if (InvokeRequired)
{
Invoke(new Action<Device>(UpdateDeviceUI), device);
return;
}
// 查找或创建对应的行
DataGridViewRow row = _grid.Rows
.Cast<DataGridViewRow>()
.FirstOrDefault(r => (int)r.Cells["ID"].Value == device.DeviceId);
if (row == null)
{
row = new DataGridViewRow();
row.CreateCells(_grid);
row.Cells[0].Value = device.DeviceId;
row.Cells[1].Value = (device is EthernetDevice) ? "以太网" : "串口";
_grid.Rows.Add(row);
}
// 更新数据
row.Cells["Status"].Value = device.Status;
row.Cells["Data"].Value = device switch
{
EthernetDevice eth => $"{eth.Temperature:F1} ℃",
SerialDevice serial => $"{serial.RPM} RPM",
_ => "N/A"
};
}
}
```
---
### 核心原理图解
```mermaid
graph TD
A[用户点击启动] --> B[DeviceManager.StartAll]
B --> C[遍历所有设备]
C --> D[Task.Run 启动设备监控]
D --> E[线程池分配线程]
E --> F[设备执行StartMonitoringAsync]
F --> G{是否取消?}
G -- 否 --> H[采集数据]
H --> I[触发DataUpdated事件]
I --> J[DeviceManager捕获事件]
J --> K[主窗体Invoke更新UI]
G -- 是 --> L[结束任务]
```
### 关键特性
1. **线程池管理**
通过 `Task.Run` 将设备监控任务提交给 .NET 线程池,自动优化线程使用
2. **统一取消机制**
`CancellationTokenSource` 实现一键停止所有设备
3. **线程安全更新**
`Control.Invoke` 确保从后台线程安全更新UI
4. **设备类型扩展**
通过继承 `Device` 基类可轻松添加新设备类型
5. **事件驱动架构**
`DataUpdated` 事件解耦设备逻辑与UI更新