一、背景
- 多线程处理通讯数据: 通常TCP通讯,开启三个线程,把接收、处理、显示分割来提高软件运行效率。
- 应用场景: 在数据采集领域,往往需要快速通讯保证数据实时性。如图像采集上位机、工业温控上位机、某些连续波形采集显示等等。
二、TCP通讯
1.声明
private Socket socketClient = null;
public Thread ThreadClient = null; //tcp主动接收线程
public string ipNum = "127.0.0.1";
public string portNum = "5678";
2.连接与断开
/// <summary>
/// 客户端套接字连接到网络节点上
/// </summary>
/// <returns></returns>
private int ConnectTCP()
{
if (SocketClient != null)
{
SocketClient.Dispose(); //释放资源
}
SocketClient = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); //因为关闭时需要释放对象,所以在连接前重新创建一个对象
try
{
IPAddress ip = StrToIPv4(ipNum);
if (ip == null)
{
return 1002; //IP地址错误
}
IPEndPoint ipe = new IPEndPoint(ip, Convert.ToInt32(portNum));
//客户端套接字连接到网络节点上,用的是Connect
SocketClient.Connect(ipe);
//开启线程
ThreadClient = new Thread(Recv);
ThreadClient.IsBackground = true;
ThreadClient.Start();
}
catch (Exception e)
{
DisconnetTCP();
isConnect = false;
return 1001; //1001连接网口失败
}
isConnect = true;
return 0;
}
/// <summary>
/// 断开TCP
/// </summary>
public void DisconnetTCP()
{
if (SocketClient != null)
{
SocketClient.Dispose(); //释放资源
}
if (ThreadClient != null)
{
ThreadClient.Abort(); //终止线程
}
isConnect = false;
}
3.TCP接收线程函数
//接收服务端发来信息的方法
private void Recv()
{
//持续监听服务端发来的消息
while (true)
{
try
{
//定义一个1M的内存缓冲区,用于临时性存储接收到的消息
byte[] arrRecvmsg = new byte[1024 * 1024];
//将客户端套接字接收到的数据存入内存缓冲区,并获取长度
int lenghtData= SocketClient.Receive(arrRecvmsg);
byte[] ReceiveData = new byte[lenghtData];
//将套接字获取到的字符数组转换为人可以看懂的字符串
// string strRevMsg = Encoding.UTF8.GetString(arrRecvmsg, 0, lenghtData);
Array.Copy(arrRecvmsg, ReceiveData, lenghtData);
//解析
AnalysisRead(strRevMsg);
}
catch (Exception ex)
{
continue;
}
}
}
以上是我们常用的Socket通讯方式。当解析函数AnalysisRead存在大量的计算、并显示结果到界面时,如果不另外开线程,数据实时性很难保证。
三、多线程接收、处理、显示
1.接收函数AnalysisRead
object AnsysPdLock = new object();
Semaphore TaskSemaphoreTCP; //TCP缓存区
private void AnalysisRead(byte[] revData)
{
try
{
lock (AnsysPdLock)
{
tcpRevQueue.Enqueue(revData); //入列
}
// 每添加一个任务,释放一个资源
TaskSemaphoreTCP.Release();
}
catch (Exception)
{
}
}
2.数据解析
Thread ThreadTCPAnalysis; //波形解析线程(TCP连接成功后开启线程)
ThreadTCPAnalysis = new Thread(AnalysisRevTCP);
ThreadTCPAnalysis.IsBackground = true;
ThreadTCPAnalysis.Start();
/// <summary>
/// 解析线程函数
/// </summary>
private void AnalysisRevTCP()
{
byte[] tepData;
while (true)
{
try
{
TaskSemaphoreTCP.WaitOne(); //等待并消耗一个信号
lock (AnsysPdLock)
{
if (tcpRevQueue.Count > 0)
{
tepData = tcpRevQueue.Dequeue(); //出列
}
else
{
continue;
}
}
AnalysisUWav(tepData); //解析函数
}
catch (Exception ex)
{
}
}
}
private List<double> uWavVol = new List<double>(); //连续采集波形图电压值
private Queue<List<double>> pdQueue = new Queue<List<double>>(); //PD值缓存区
Semaphore TaskSemaphoreWav; //波形线程缓存区
object TCPReciveLock = new object();
/// <summary>
/// 解析连续PD
/// </summary>
/// <param name="revData"></param>
private void AnalysisUWav(List<byte> revData)
{
int dataLength = revData.Count / 2;
try
{
for (int i = 0; i < dataLength; i++)
{
double vol = Convert.ToDouble((Convert.ToInt32(revData[i * 2] << 8) + revData[1 + i * 2])) / 1000;
uWavVol.Add(vol);
uWavVol.RemoveAt(0);
}
lock (TCPReciveLock)
{
pdQueue.Enqueue(uWavVol); //入列
}
// 每添加一个任务,释放一个资源
TaskSemaphoreWav.Release();
}
catch (Exception ex)
{
//LogWrite.logWrite(ex.Message, ex.StackTrace); //写入日志
}
}
3.数据显示
Thread ThreadRefreshWav; //波形显示线程
ThreadRefreshWav = new Thread(FunRefreshWav);
ThreadRefreshWav.Name = "RefreshWav";
ThreadRefreshWav.IsBackground = true;
ThreadRefreshWav.Start();
/// <summary>
/// 波形显示函数
/// </summary>
private void FunRefreshWav()
{
while (true)
{
List<double> uWav = new List<double>();
try
{
TaskSemaphoreWav.WaitOne(); //等待并消耗一个信号
lock (AnsysPdLock)
{
if (pdQueue.Count > 0)
{
uWav = pdQueue.Dequeue(); //出列
}
else
{
continue;
}
}
if (chartDebugWav.InvokeRequired)
{
chartDebugWav.Invoke(new Action(() => {
chartDebugWav.Series[0].Points.DataBindY(uWav);
}));
}
else
{
chartDebugWav.Series[0].Points.DataBindY(uWav);
}
}
catch (Exception ex)
{
}
}
}
四、总结
1.整体的思路:
- 开启三个线程和2个队列缓存
- 线程1负责实时监控TCP接收数据,收到数据后立马给队列1进站该数据
- 线程2实时监控队列1,当队列1存在数据立马取出并解析,把解析结果放进队列2
- 线程3实时监控队列2,当队列2有数据立马取出显示到界面
2.Queue:
- 队列作为一种基础且实用的数据结构,遵循“先进先出”(First-In, First-Out, FIFO)原则,广泛应用于各种编程场景。
- Queue不是线程安全列表,因此需要借用信号量Semaphore来实现同步。
注:
转载本文需要标明出处!
六七彭(原谷子彭):[email protected]