C# 勇哥关于多线程读写plc内存的研究续,解决lock锁的效率问题

勇哥注:

我们继续上一篇勇哥关于多线程读写plc内存的研究

在上篇结尾,我们看到lock锁带来的效率问题。

本着程序员应有的工匠精神,本节我们来研究并解决这个问题。


先回顾一下上篇文章结尾勇哥截的程序效率图。

Monitor.Enter的函数占用cpu时间75.85%,此耗能大户就是lock锁。(因为lock关键字实际上是Monitor的语法糖)

image.png

image.png


由于plc读写操作中,读的次数一定远大于写入的次数,因此我们希望的是,无论有多少个线程都可以一起读,

只是同一时间只能有一个线程能写。而lock是独占,它区分不了读与写的不同,反正不管是读还是写,只要有一个线程在操作,

其它的线程就只有等待。

按上面的分析,我们使用lock是不合理的。


要达到这个要求,可以使用ReaderWriterLockSlim锁,它的特点是多个线程可以读,但只允许一个线程写入

(必须等读线程完成后才可以),因为我们就是希望读与写是互斥的,所以能满足我们的要求。


改动后代码如下:


代码仅仅是在Read()和Write()函数中做了一些修改。

有关ReaderWriterLockSlim锁的知识,请看本站另一篇文章C#读写锁ReaderWriterLockSlim


using netMarketing.automation.plc.Omron.FinsLibrary;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace plcreadmode
{
    public class PlcReadWrite
    {
        static private ReaderWriterLockSlim rwlock = new ReaderWriterLockSlim();
        
        public static log plclog = new log();

        static List<int> MemBuffer = new List<int>();

        /// <summary>
        /// 读plc的指令列表
        /// </summary>
        static List<ReadCmd> ReadCmdList = new List<ReadCmd>();
        /// <summary>
        /// 写plc的指令列表
        /// </summary>
        static List<WriteCmd> WriteCmdList = new List<WriteCmd>();

        static Thread t1 = new Thread(new ThreadStart(doWork)) { IsBackground = true };

        static readonly ManualResetEvent[] mr = new ManualResetEvent[2];

        

        static Stopwatch sw1 = new Stopwatch();
        static Stopwatch sw2 = new Stopwatch();

        static FinsSocket cp1h = null;

        /// <summary>
        /// 读写起始地址
        /// </summary>
        static public ushort StartAddr { get; set; } = 2000;

        /// <summary>
        /// 读写最大数量
        /// </summary>
        static public ushort ReadWriteCount { get; set; } = 4;

        static public string PlcIPAddr { get; set; } = "192.168.250.1";

        static public int PlcPort { get; set; } = 9600;

        static bool isRead { get; set; } = false;

        private static readonly object readwriteLock = new object();


        static  PlcReadWrite()
        {
            if (!t1.IsAlive)
            {
                cp1h = new FinsSocket();
                cp1h.IP = PlcIPAddr;
                cp1h.Port = PlcPort; 

                for (int i=0;i<ReadWriteCount;i++)
                {
                    MemBuffer.Add(0);
                }
                mr[0] = new ManualResetEvent(false);
                mr[1] = new ManualResetEvent(false);
                sw1.Start();
                sw2.Start();
                t1.Start();
            }
        }

        static private void doWork()
        {
            while (true)
            {
                mr[0].WaitOne();
                if (isRead)
                {
                    readPlc();
                }
                else
                {
                    writePlc();
                }
                mr[0].Reset();
            }
        }


        static void writePlc()
        {
            var writeResult = false;
            if (WriteCmdList.Count > 0 && WriteCmdList.Count <= 3)
            {
                //单个写
                for (int i = 0; i < WriteCmdList.Count; i++)
                {
                    var offsetAddr = WriteCmdList[i].addr;
                    var data = WriteCmdList[i].writeData;
                    writeResult=cp1h.Write(_eMemory.DM, (ushort)(StartAddr + offsetAddr), data);
                    WriteCmdList[i].writeStatus = writeResult?1:0;
                }
            }
            else
            {
                var list1 = new List<short>();
                for (int i = 0; i < ReadWriteCount; i++)
                {
                    list1.Add(0);
                }
                var plcdataBuffer = list1.ToArray();

                //多个写
                if (WriteCmdList.Count >= ReadWriteCount)
                {
                    for (int i = 0; i < ReadWriteCount; i++)
                    {
                        plcdataBuffer[i] = (short)WriteCmdList[i].writeData;
                    }
                    //如果要写的数量刚好等于读写最大数量ReadWriteCount,则直接写
                    writeResult=cp1h.WriteMultiple(_eMemory.DM, StartAddr, plcdataBuffer);
                }
                else
                {
                    //先读出全部地址数据
                    cp1h.Read(_eMemory.DM, StartAddr, ref plcdataBuffer);

                    //再由待写数据更新缓存,再把缓存一次写入地址
                    for(int i=0;i< ReadWriteCount;i++)
                    {
                        plcdataBuffer[WriteCmdList[i].addr] = (short)WriteCmdList[i].writeData;
                    }
                    writeResult=cp1h.WriteMultiple(_eMemory.DM, StartAddr, plcdataBuffer);
                }
                for (int i = 0; i < WriteCmdList.Count; i++)
                {
                    WriteCmdList[i].writeStatus = writeResult?1:0;
                }
            }
        }

        static void readPlc()
        {
            var list1 = new List<short>();
            for(int i=0;i< ReadWriteCount;i++)
            {
                list1.Add(0);
            }
            var plcdataF = list1.ToArray();
            var readResult = cp1h.Read(_eMemory.DM, StartAddr, ref plcdataF);

            for (int i = 0; i < ReadCmdList.Count; i++)
            {
                if (readResult)
                {
                    ReadCmdList[i].readStatus = 1;
                    ReadCmdList[i].readResult = plcdataF[ReadCmdList[i].addr];
                }
                else
                {
                    ReadCmdList[i].readStatus = 0;
                    ReadCmdList[i].readResult = -1;
                }
            }
        }

      

        public int Write(int addr,short data, Guid _id)
        {
            rwlock.EnterWriteLock();
            try
            {
                var writeSw = new Stopwatch();
                writeSw.Start();
                isRead = false;
                WriteCmdList.Add(new WriteCmd()
                {
                    addr = addr,
                    id = _id,
                    writeData = data,
                    writeStatus = -1
                });
                var idx = -1;
                int js1 = 0;
                while (true)
                {
                    if (writeSw.ElapsedMilliseconds > 2000)
                    {
                        //读plc超时
                        writeSw.Stop(); return -1;
                    }
                    if (sw2.ElapsedMilliseconds > 50 && js1 == 0)
                    {
                        //通知写plc
                        mr[0].Set(); sw2.Restart(); ++js1;
                    }
                    idx = WriteCmdList.FindIndex(s => s.id == _id && s.writeStatus == 1);
                    if (idx >= 0)
                    {
                        //写成功了,返回1
                        WriteCmdList.RemoveAt(idx);
                        plclog.AddLog(new logdata()
                        {
                            id = _id,
                            readOrWrite = readwriteEnum.write,
                            plcAddr = (short)addr,
                            recTime = DateTime.Now,
                            itemCount =WriteCmdList.Count
                        });
                        return 1;
                    }
                    Thread.Sleep(1);
                }
            }
            finally
            {
                rwlock.ExitWriteLock();
            }
        }

        public int Read(int addr,Guid _id)
        {
            rwlock.EnterReadLock();
            try
            {
                var readSw = new Stopwatch();
                readSw.Start();
                isRead = true;
                ReadCmdList.Add(new ReadCmd()
                {
                    id = _id,
                    addr = addr,
                    readResult = -1,
                    readStatus = 0
                });
                var idx = -1;
                int js1 = 0;
                while (true)
                {
                    if(readSw.ElapsedMilliseconds>2000)
                    {
                        //读plc超时
                        readSw.Stop(); return -1;
                    }
                    if (sw1.ElapsedMilliseconds > 50 && js1 == 0)
                    {
                        //通知读plc
                        mr[0].Set(); sw1.Restart(); ++js1;
                    }
                    idx = ReadCmdList.FindIndex(s => s.id == _id && s.readStatus == 1);
                    if (idx >= 0)
                    {
                        //读成功后返回读到的值
                        var resData = ReadCmdList[idx].readResult;
                        ReadCmdList.RemoveAt(idx);

                        plclog.AddLog(new logdata()
                        {
                            id = _id,
                            readOrWrite = readwriteEnum.read,
                            plcAddr = (short)addr,
                            recTime = DateTime.Now,
                             itemCount= ReadCmdList.Count
                        });
                        return resData;
                    }
                    
                    Thread.Sleep(1);
                }
            }
            finally
            {
                rwlock.ExitReadLock();
            }
       
        }

      
    }

    public class WriteCmd
    {
        public Guid id { get; set; }
        public int addr { get; set; } = -1;
        public short writeData { get; set; } = 0;
        public int writeStatus { get; set; } = 0;
    }

    public class ReadCmd
    {
        public Guid id { get; set; } 
        public int addr { get; set; } = -1;
        public int readResult { get; set; } = 0;
        public int readStatus { get; set; } = 0;
    }


}


我们再来分析一下效率的提升。

占cpu时间最多的第一名 GetHostAddresses函数,实际上是读写plc的网络操作,这个是系统操作。不关我们的事。

Application.Run这个是整winform程序占用的总时间,这次是17.79%, 上篇文章中是2.69%,不过不能说是增加,只是对比的基数不同而已。

其它跟我们能扯上点有关系的就是RichTextBox.set_Text,占比6.44%,这个是控件更新造成的。 这个我们可以继续优化一下。

image.png


下图是写控件的耗能情况,确实值得再优化一下。

image.png

其它一些语句的耗能情况如下:

image.png


总得来讲,效率比起lock来说是提升了一大截。

在讲读写锁ReaderWriterLockSlim那篇文章,讲了一组纸面数据,很能说明问题:

就500个Task,每个Task占用一个线程池线程,其中20个写入线程和480个读取线程,
模拟操作。其中读取数据花10ms,写入操作花100ms,分别测试了对于lock方式和ReaderWriterLockSlim方式。
可以做一个估算,对于ReaderWriterLockSlim,假设480个线程同时读取,那么消耗10ms,
20个写入操作占用2000ms,因此所消耗时间2010ms,
而对于普通的lock方式,由于都是独占性的,因此480个读取操作占时间4800ms+20个写入操作2000ms=6800ms。
运行结果显示了性能提升明显


勇哥2022/11/2注:


上面程序今天执行时出现下面的报错。

想了一下,这应该是使用ReaderWriterLockSlim锁后副作用出现了。

因为该锁允许N个线程同时读,假设执行ReadCmdList.RemoveAt(idx) 这句的线程A,它要删除的索引是2,

但是可能同一时间另一个线程B也执行了此句,它先删除了索引1。

当线程A要删除索引2的时候,已经没有索引2了,它现在列表中的索引变化了。

因此这是多线程删除集合元素时产生的问题。

显然如果是lock,是不会出现这种问题的,这个时候各位是不是又觉得傻瓜方式的lock确实易用得多?

image.png


怎么解决这个问题?

由于C#没有线程安全的List<T>,所以勇哥现在很苦恼。。。


想来想去,有一个办法,就是把删除ReadCmdList内容的时机放在Write函数里。

因为ReaderWriterLockSlim锁的原因,当正在执行写操作的时候,所有Read的线程都会被锁住进行排队等待。这个时机我们用来删除ReadCmdList的项目。


1. 首先定义一个List<Guid>用来记录Read中要删除的项目,按id

2. 在Read中不要做ReadCmdList.RemoveAt(idx); 而是把要删除的项目id压入readyRemoveItems。

3. 在Write中按readyRemoveItems记录的项目进行删除。此时删除是安全的。


        /// <summary>
        /// 要删除的项目列表
        /// </summary>
        static List<Guid> readyRemoveItems = new List<Guid>();

(第一步)

image.png

(第二步)

image.png

(第三步)


但是这样做又有一个新问题,如果整个类,只读不写,则没机会删除ReadCmdList的内容了,最终会撑爆列表。

怎么办呢?

。。。。


那就只剩下一招了,在这个类里加一个ClearReadCmdList() 成员函数,让调用者自己在适合的时机去清除了。

因为想自动时机清除,臣妄做不到啊~~~


注意: 这里勇哥是从程序的效率出发在考虑这个问题,否则,如果抛开效率,我们解决这种问题办法还是很多的。


--------------------- 

作者:hackpig

来源:www.skcircle.com

版权声明:本文为博主原创文章,转载请附上博文链接!


本文出自勇哥的网站《少有人走的路》wwww.skcircle.com,转载请注明出处!讨论可扫码加群:

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

会员中心
搜索
«    2025年4月    »
123456
78910111213
14151617181920
21222324252627
282930
网站分类
标签列表
最新留言
    热门文章 | 热评文章 | 随机文章
文章归档
友情链接
  • 订阅本站的 RSS 2.0 新闻聚合
  • 扫描加本站机器视觉QQ群,验证答案为:halcon勇哥的机器视觉
  • 点击查阅微信群二维码
  • 扫描加勇哥的非标自动化群,验证答案:C#/C++/VB勇哥的非标自动化群
  • 扫描加站长微信:站长微信:abc496103864
  • 扫描加站长QQ:
  • 扫描赞赏本站:
  • 留言板:

Powered By Z-BlogPHP 1.7.2

Copyright Your skcircle.com Rights Reserved.

鄂ICP备18008319号


站长QQ:496103864 微信:abc496103864