异步设备DLL开发指南
# 异步设备DLL开发指南
为提升 I/O 密集型设备的并发性能,协议插件支持以异步方式开发:继承 CEquipBaseAsync 基类并实现 IEquipAsync 接口后,设备驱动在等待数据期间释放线程,把线程让给其他任务。本文介绍异步设备 DLL 的开发步骤、性能优化建议与常见问题。
前置知识
C# async/await、Task 异步编程基础。适用对象:设备驱动 DLL 开发者。
# 概述
# 什么是异步DLL
异步 DLL 是继承自 CEquipBaseAsync 基类的设备驱动程序,通过实现 IEquipAsync 接口,支持真正的异步 I/O 操作,能够在数据采集时释放线程资源,提高系统的并发性能。
# 为什么使用异步
| 传统同步 DLL | 异步 DLL |
|---|---|
| 阻塞线程等待 I/O | 释放线程,不阻塞 |
| 串口/网络等待时线程空闲 | 线程可处理其他任务 |
| 系统并发能力受限 | 高并发场景性能更好 |
| 适合快速内存操作 | 适合 I/O 密集型操作 |
# 适用场景
✅ 推荐使用异步的场景:
- 网络通讯(TCP、HTTP、Modbus TCP 等)
- 串口通讯(存在延迟的场景)
- 文件读写操作
- 数据库查询
- 任何涉及等待外部响应的场景
❌ 不推荐异步的场景:
- 纯内存计算(模拟设备)
- 极快速的操作(微秒级)
- 本地变量读写
# 基类架构
# 继承关系
IEquip (接口)
↓
CEquipBase (同步基类)
↓
CEquipBaseAsync (异步基类) : CEquipBase, IEquipAsync
↓
您的设备类 (CEquip)
# 开发指南
# 第1步:创建项目和类
using GWDataCenter;
using GWDataCenter.Database;
using System.Threading.Tasks;
namespace YourCompany.DeviceDriver
{
/// <summary>
/// 设备通讯类(异步版本)
/// 类名必须是 "CEquip"
/// 必须继承 CEquipBaseAsync
/// </summary>
public class CEquip : CEquipBaseAsync
{
public CEquip() : base()
{
}
}
}
类名必须是 CEquip,且必须继承 CEquipBaseAsync。
# 第2步:重写初始化方法 InitAsync
✅ 正确做法(推荐):
public override async Task<bool> InitAsync(EquipItem item)
{
// 1. 先调用基类的异步初始化(不会死锁)
if (!await base.InitAsync(item))
return false;
// 2. 添加自己的初始化逻辑
// 解析配置参数
if (!string.IsNullOrEmpty(item.communication_param))
{
// 解析设备参数
ParseDeviceParams(item.communication_param);
}
// 3. 初始化连接(如果需要异步操作)
if (_needConnection)
{
await InitializeConnectionAsync();
}
return true;
}
⚠️ 错误做法(会导致问题):
// ❌ 错误1:调用同步方法 base.init()
public override async Task<bool> InitAsync(EquipItem item)
{
if (!base.init(item)) // ❌ 不推荐,虽然能工作但不一致
return false;
// ...
}
// ❌ 错误2:在同步方法中同步等待异步方法
public override bool init(EquipItem item)
{
// ❌ 会死锁!
return InitAsync(item).GetAwaiter().GetResult();
}
# 第3步:重写数据采集方法 GetDataAsync
有两种重写方式。
# 方式 A:重写 GetDataAsync,完全控制采集流程
public override async Task<CommunicationState> GetDataAsync(CEquipBase pEquip)
{
// 使用异步休眠替代同步休眠
await SleepAsync(500); // ✅ 不阻塞线程
// 检查设置标志
if (RunSetParmFlag)
return CommunicationState.setreturn;
// 建立连接(如果需要)
if (!await ConnectAsync())
return CommunicationState.fail;
// 采集YC数据
if (ycprows != null)
{
foreach (var r in ycprows)
{
if (RunSetParmFlag)
return CommunicationState.setreturn;
if (!await GetYCAsync(r))
return CommunicationState.fail;
}
}
// 采集YX数据
if (yxprows != null)
{
foreach (var r in yxprows)
{
if (RunSetParmFlag)
return CommunicationState.setreturn;
if (!await GetYXAsync(r))
return CommunicationState.fail;
}
}
return CommunicationState.ok;
}
# 方式 B:重写 GetYCAsync / GetYXAsync,使用基类采集流程
public override async Task<CommunicationState> GetDataAsync(CEquipBase pEquip)
{
// 1. 使用异步休眠
await SleepAsync(500);
// 2. 调用基类的采集流程(会自动调用 GetYCAsync 和 GetYXAsync)
return await base.GetDataAsync(pEquip);
}
// 重写YC数据采集
protected override async Task<bool> GetYCAsync(YcpTableRow r)
{
// 异步读取设备数据
var value = await ReadFromDeviceAsync(r.main_instruction, r.minor_instruction);
// 设置数据
SetYCData(r, value);
return true;
}
// 重写YX数据采集
protected override async Task<bool> GetYXAsync(YxpTableRow r)
{
// 异步读取设备状态
var state = await ReadStateFromDeviceAsync(r.main_instruction);
// 设置数据
SetYXData(r, state);
return true;
}
# 第4步:重写设置参数方法 SetParmAsync
public override async Task<bool> SetParmAsync(string cmd1, string cmd2, string value)
{
try
{
// 异步发送设置命令到设备
await SendCommandAsync(cmd1, cmd2, value);
// 等待设备响应
var response = await WaitForResponseAsync(timeout: 5000);
if (response.IsSuccess)
{
// 生成成功事件
var evt = new EquipEvent($"设置成功:{cmd1}", MessageLevel.Info, DateTime.Now);
EquipEventList.Add(evt);
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
DataCenter.Write2Log($"设置失败:{ex.Message}", LogLevel.Error);
return false;
}
}
# 性能优化
# 1. 选择合适的异步模式
// ✅ 真实I/O设备 - 使用真正的异步
protected override async Task<bool> GetYCAsync(YcpTableRow r)
{
// 网络读取(真正的异步I/O)
var value = await _tcpClient.ReadAsync(buffer, 0, length);
SetYCData(r, value);
return true;
}
// ⚠️ 模拟设备或快速操作 - 考虑同步
protected override async Task<bool> GetYCAsync(YcpTableRow r)
{
// 只是内存操作,异步开销可能更大
await Task.Yield(); // 让出线程即可
double value = CalculateValue(r); // 纯内存计算
SetYCData(r, value);
return true;
}
# 2. 使用 SleepAsync 替代 Sleep
// ❌ 阻塞线程
public override async Task<CommunicationState> GetDataAsync(CEquipBase pEquip)
{
Sleep(1000); // 阻塞当前线程1秒
// ...
}
// ✅ 释放线程
public override async Task<CommunicationState> GetDataAsync(CEquipBase pEquip)
{
await SleepAsync(1000); // 线程被释放,可以处理其他任务
// ...
}
# 3. 合理使用 Task.Yield
// 场景1:纯内存操作,但想支持异步接口
protected override async Task<bool> GetYCAsync(YcpTableRow r)
{
await Task.Yield(); // 让出线程给其他任务
// 快速的内存操作
double value = _random.Next(100);
SetYCData(r, value);
return true;
}
// 场景2:长时间CPU计算,定期让出线程
private async Task<double> ComplexCalculationAsync(YcpTableRow r)
{
double result = 0;
for (int i = 0; i < 1000000; i++)
{
result += Math.Sin(i);
// 每1000次让出线程
if (i % 1000 == 0)
await Task.Yield();
}
return result;
}
# 完整示例:TCP 网络设备
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
using GWDataCenter;
using GWDataCenter.Database;
public class CEquip : CEquipBaseAsync
{
private TcpClient _tcpClient;
private NetworkStream _stream;
private string _ipAddress;
private int _port;
public CEquip() : base()
{
}
public override async Task<bool> InitAsync(EquipItem item)
{
// 调用基类初始化
if (!await base.InitAsync(item))
return false;
// 解析IP和端口
var parts = item.communication_param.Split(':');
_ipAddress = parts[0];
_port = int.Parse(parts[1]);
// 异步连接
try
{
_tcpClient = new TcpClient();
await _tcpClient.ConnectAsync(_ipAddress, _port);
_stream = _tcpClient.GetStream();
DataCenter.Write2Log($"设备 {item.iEquipno} 连接成功: {_ipAddress}:{_port}");
return true;
}
catch (Exception ex)
{
DataCenter.Write2Log($"设备 {item.iEquipno} 连接失败: {ex.Message}", LogLevel.Error);
return false;
}
}
public override async Task<CommunicationState> GetDataAsync(CEquipBase pEquip)
{
try
{
// 检查设置标志
if (RunSetParmFlag)
return CommunicationState.setreturn;
// 调用基类流程(会自动调用 GetYCAsync 和 GetYXAsync)
return await base.GetDataAsync(pEquip);
}
catch (Exception ex)
{
DataCenter.Write2Log($"数据采集异常: {ex.Message}", LogLevel.Error);
return CommunicationState.fail;
}
}
protected override async Task<bool> GetYCAsync(YcpTableRow r)
{
try
{
// 构造Modbus读取命令
byte[] command = BuildModbusCommand(r.main_instruction, r.minor_instruction);
// 异步发送命令
await _stream.WriteAsync(command, 0, command.Length);
// 异步读取响应
byte[] buffer = new byte[256];
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
// 解析数据
double value = ParseModbusResponse(buffer, bytesRead);
SetYCData(r, value);
return true;
}
catch (Exception ex)
{
DataCenter.Write2Log($"读取YC数据失败: {ex.Message}", LogLevel.Error);
return false;
}
}
protected override async Task<bool> GetYXAsync(YxpTableRow r)
{
// 类似 GetYCAsync 的实现
await Task.Delay(10); // 模拟读取
SetYXData(r, true);
return true;
}
public override async Task<bool> SetParmAsync(string cmd1, string cmd2, string value)
{
try
{
// 构造设置命令
byte[] command = BuildSetCommand(cmd1, cmd2, value);
// 异步发送
await _stream.WriteAsync(command, 0, command.Length);
// 等待响应
byte[] buffer = new byte[128];
int bytesRead = await _stream.ReadAsync(buffer, 0, buffer.Length);
return bytesRead > 0;
}
catch (Exception ex)
{
DataCenter.Write2Log($"设置参数失败: {ex.Message}", LogLevel.Error);
return false;
}
}
// 辅助方法
private byte[] BuildModbusCommand(string cmd1, string cmd2)
{
// 实现Modbus命令构造
return new byte[] { 0x01, 0x03, 0x00, 0x00, 0x00, 0x01 };
}
private double ParseModbusResponse(byte[] buffer, int length)
{
// 实现Modbus响应解析
return BitConverter.ToInt16(buffer, 3);
}
private byte[] BuildSetCommand(string cmd1, string cmd2, string value)
{
// 实现设置命令构造
return new byte[] { 0x01, 0x06 };
}
// 同步兼容方法
public override bool init(EquipItem item)
{
return InitAsync(item).GetAwaiter().GetResult();
}
public override CommunicationState GetData(CEquipBase pEquip)
{
return GetDataAsync(pEquip).GetAwaiter().GetResult();
}
public override bool SetParm(string cmd1, string cmd2, string value)
{
return SetParmAsync(cmd1, cmd2, value).GetAwaiter().GetResult();
}
}
# 常见问题
# Q1:我的DLL需要改为异步吗?
根据场景决定:
| 场景 | 是否需要异步 | 原因 |
|---|---|---|
| 网络通讯(TCP/HTTP) | ✅ 强烈推荐 | 网络延迟高,异步可提升并发 |
| 串口通讯 | ✅ 推荐 | 串口等待期间释放线程 |
| 模拟设备(纯内存) | ❌ 不推荐 | 无 I/O 等待,异步开销更大 |
| 快速 Modbus 读取 | ⚖️ 可选 | 根据实际性能测试决定 |
| 数据库查询 | ✅ 推荐 | 数据库操作天然适合异步 |
# Q2:如何判断DLL是异步还是同步?
系统启动时会在日志中显示:
设备 1 使用异步模式 (DLL实现了IEquipAsync接口)
设备 2 使用同步模式 (传统DLL)
也可以通过性能报告查看:
- 异步设备:模式列显示"异步"
- 同步设备:模式列显示"同步"
# Q3:可以在同步方法中调用异步方法吗?
可以,但要小心死锁:
// ✅ 方法1:使用 Task.Run 隔离(推荐)
public override bool init(EquipItem item)
{
return Task.Run(async () => await InitAsync(item)).GetAwaiter().GetResult();
}
// ❌ 方法2:直接等待(可能死锁)
public override bool init(EquipItem item)
{
return InitAsync(item).GetAwaiter().GetResult(); // 危险!
}
// ✅ 方法3:调用基类的同步方法或 InitializeCore(最安全)
public override bool init(EquipItem item)
{
return InitializeCore(item); // 推荐
}
# Q4:Sleep 和 SleepAsync 有什么区别?
| 方法 | 行为 | 适用场景 |
|---|---|---|
| Sleep(1000) | 阻塞当前线程1秒 | 同步方法、模拟设备 |
| await SleepAsync(1000) | 释放线程,1秒后继续 | 异步方法、真实 I/O |
性能对比(100 个设备):
- 使用 Sleep:需要 100 个线程,每个阻塞 1 秒
- 使用 SleepAsync:只需要少量线程,轮流处理
# Q5:老版本的DLL能继续使用吗?
可以,完全兼容:
- 老 DLL 继承 CEquipBase,系统识别为同步模式
- 新 DLL 继承 CEquipBaseAsync,系统识别为异步模式
- 两种 DLL 可以在同一个系统中混用
- 系统会自动调用对应的同步/异步方法
# 最佳实践总结
# 推荐做法
- 继承 CEquipBaseAsync 并重写异步方法
- 调用 await base.InitAsync() 而不是 base.init()
- 使用 await SleepAsync() 替代 Sleep()
- 在异步方法中使用 await Task.Delay() 或真实异步 I/O
- 提供同步兼容方法(调用异步版本)
- 添加详细的日志和异常处理
- 根据设备类型选择合适的模式(真实 I/O 用异步,模拟用同步)
# 避免的错误
- 在同步方法中同步等待异步方法(可能死锁)
- 在 lock 内使用 await(编译错误)
- GetYCAsync 和 GetYC 都有延迟(双重延迟)
- 纯内存操作强行使用异步(性能下降)
- 不提供同步兼容方法(兼容性问题)
上次更新: 2026/10/10 17:08:35
← 进阶 Java协议插件开发实践→