C# 勇哥关于多线程读写plc内存的研究续,引用ReaderWriterLockSlim锁带来的读操作并发的问题

勇哥注:

我们继续上一篇C# 勇哥关于多线程读写plc内存的研究续,解决UI控件读写的效率问题


此系列贴子已经写了好几篇了:

(3)C# 勇哥关于多线程读写plc内存的研究续,解决UI控件读写的效率问题

http://47.98.154.65/?id=1985


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

http://47.98.154.65/?id=1983


(1) C# 勇哥关于多线程读写plc内存的研究

http://47.98.154.65/?id=1981


在第(3)篇里勇哥牛皮已经吹出去了,说这个plc多线程类已经比较好用了。

实际情况是,在引入了ReaderWriterLockSlim锁后,因为读操作变成并发方式,长时间运行会产生新的问题~~~

当初认为可以了,实际上只是测试的时间不够长而矣。


问题一:


ReadCmdList列表保存的是读指令。

但是会出现ReadCmdList[i]==null的情况。

这个是为什么呢?

image.png

这一种情况是多个线程在执行Read(),所以相当于同时向ReadCmdList进行Add

但是由于List的add执行时,动作分为两步,一是容器大小先加1,二是把新数据进行赋值。

因为上图中线程刚好在一个add的第一步时进行读操作,因此会读到元素为null。


此问题网上有一篇博文讨论过,如下:

多线程并发导致List的add()失败,元素为null

image.png

总结一下就是: List.Add 方法自己都不是一个原子操作,要经过1,2,3.....步操作。
只有它做到最后一步时,你的线程去读,才能读到正确的结果。
当add只操作到第1步的时候,碰巧你的线程非要去读,于是完蛋了,得到null。


问题二:


对于List的add方法,居然报了索引超出了数组界限??

是不是很奇怪。

image.png

可以看到并不是logBuffer超内存,它才35个元素。

image.png

传入的data,其数据结构中并没有数组。

image.png


勇哥查了一下关于异常System.IndexOutofRangeException 的官方解释。

其有这么一条:

违反线程安全性。 从同一 StreamReader 个对象读取、从多个线程写入同 StreamWriter 一 Hashtable 
对象或从不同线程枚举对象等操作,如果无法以线程安全的方式访问对象,则可能会引发 IndexOutOfRangeException 该对象。 
此异常通常是间歇性的,因为它依赖于争用条件。

官方文档见: https://learn.microsoft.com/zh-cn/dotnet/api/system.indexoutofrangeexception?view=netframework-4.7.2


勇哥的理解是,List<T>不是线程安全的。

查阅一下官方文档关于List<T>的解释: https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.list-1?view=netframework-4.7.2


关于其线程安全说明如下:

线程安全性
此类型的Visual Basic) 成员中的公共静态 (Shared是线程安全的。 但不保证所有实例成员都是线程安全的。
可以安全地对某个 List<T>集合执行多个读取操作,但如果正在读取集合时修改集合,则可能会出现问题。 
若要确保线程安全,请在读取或写入操作期间锁定集合。 若要允许多个线程访问集合以读取和写入,必须实现自己的同步。
有关具有内置同步的集合,请参阅命名空间中的 System.Collections.Concurrent 类。 
有关固有线程安全的替代方法,请参阅 ImmutableList<T> 该类。

这说明List<T>只是读取是线程安全,但是像什么Add方法也是线程不安全的,需要自己加锁。或者改用其它线程安全的类型。


文章讲的ImmutableList<T> 类,看上去即有Add方法,也有Remove方法,看上去很美好。(要知道Concurrent类型的集合虽然有8种之多,但是有一个问题是没有Remove方法。)

但是此集合.net Framework不能直接使用,需要Nuget进行安装。


注解
使用不可变集合,你可以:
。共享集合,使其使用者可以确保集合永远不会发生更改。
。提供多线程应用程序中的隐式线程安全(无需锁来访问集合)。
。遵循函数编程做法。
。在枚举过程中修改集合,同时确保该原始集合不会更改。

.NET Core 和 .NET 5 + 中均提供不可变的集合类,但它们不是随 .NET Framework 一起分发的核心类库的一部分。 从 .NET Framework 4.5 开始,可以通过 NuGet 开始使用。
通过 NuGet 安装不可变集合:
在 Visual Studio 中打开项目,然后从 " 项目 " 菜单中选择 " 管理 NuGet 包 "。
还可以选择 " 包括预发行 版" 复选框。
此选项可让你访问不可变类的新预发布版本,因为这些版本可用。
使用 " 搜索 " 框找到 "" 。
在左窗格中,选择 " system.web " 包。 在右侧窗格中,选择所需的版本,然后选择 "安装"。
上述安装步骤适用于 Visual Studio 2015。 对于其他版本的 Visual Studio,这些步骤可能稍有不同,因为用户界面 (UI) 不同。

但是这个说明中搜索的“system.web”是错误的,也许是微软的说明是机译版本的原因吧。

正确的是搜索下面的关键词。

第一个1.11g下载的是.net Core用的。第三个那个才是.net Framwork用的。

(这是插一句题外话, 从下载量来看,.net Core已经远远超过 .net Framework了,看来要变天了呀?)

image.png


勇哥用不可变对象集合ImmutableList<T> 改造了一下程序。

有关此集合的基本用法,请看勇哥的另几篇贴子

http://47.98.154.65/?id=1997

http://47.98.154.65/?id=1996


跑起来后,在Read()函数中,时间一长还是出现问题,例如下图所示的错误。

也可能出现在:

 var resData = ReadCmdList[idx].readResult;

这一句的错误可能出现idx超出ReadCmdList的最大值。

而图片所示的错误,可能是因为RemoveAt后,又做一了次赋值,更新ReadCmdList。

ReadCmdList= ReadCmdList.RemoveAt(idx);

如果把这个赋值取消,变成:

ReadCmdList.RemoveAt(idx);

则没有问题。但是这样的话,ReadCmdList就无法更新记录了。

所以多线程时,ReadCmdList删除记录时怎么保证不会报错,还是一个未知的问题。


image.png


并且,此集合应用时存在大量的元素数量被改变,开销也是很大的。

因此也不适合使用此类集合。


考虑到ReadCmdList的数据结构中有使用id,还是使用的uid做为唯一标识,所以勇哥考虑尝试使用ConcurrentDictionary<>线程安全字典。

此集合有多种类型,统称为Concurrent安全集合,ConcurrentDictionary<>只是其中之一。

如下表所示:

image.png

详细见微软官方说明:

https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.concurrent?view=net-6.0


这类集合已经是.net Framework自带了。不需要Nuget额外安装。


经过改造后的源码,成功的杜绝了上面的多线程造成的问题。长时间运行无报错。

耗能也下降到最低,如下图所示:

另外,勇哥发现,在死循环中进行Thread.Sleep(), 取值1和取值10相差10%的耗能,即值越大耗能越少。


image.png


代码主要改变的地方:


ReadCmdList的定义

WriteCmdList的定义没有改变

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

readPlc()

   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);
            list666.Add($"{plcdataF[0]},{plcdataF[1]},{plcdataF[2]},{plcdataF[3]}");

            var s1 = ReadCmdList;
            for (int i = 0; i < ReadCmdList.Count; i++)
            {
                ReadCmdList.ElementAt(i).Value.readStatus = 1;
                ReadCmdList.ElementAt(i).Value.readResult = plcdataF[ReadCmdList.ElementAt(i).Value.addr];
            }
            mr[1].Set();
            
        }

Read()

  /// <summary>
        /// 读plc内存地址
        /// </summary>
        /// <param name="addr">plc内存地址,填写相对地址,从0开始</param>
        /// <param name="_id">guid</param>
        /// <param name="threadId">线程id</param>
        /// <returns></returns>
        public int Read(int addr,Guid _id,int threadId)
        {
           
            rwlock.EnterReadLock();
            {
                try
                {
                    var readSw = new Stopwatch();
                    readSw.Start();
                    isRead = true;

                    ReadCmdList.TryAdd(_id, new ReadCmd()
                    {
                        threadID = threadId,
                        id = _id,
                        addr = addr,
                        readResult = -1,
                        readStatus = 0
                    });
                
                    int js1 = 0;
                    while (true)
                    {
                        mr[1].WaitOne();
                        if(!readSw.IsRunning || readSw.ElapsedMilliseconds>60)
                        {
                            readSw.Restart();
                        }
                        if (readSw.ElapsedMilliseconds > 2000)
                        {
                            //读plc超时
                            readSw.Stop(); return -1;
                        }
                        if (sw1.ElapsedMilliseconds > 50 && js1 == 0)
                        {
                            //通知读plc
                            ReadCmdList[_id].swTime = sw1.ElapsedMilliseconds;
                            mr[1].Reset();
                            mr[0].Set(); sw1.Restart(); ++js1;
                        }

                        if(ReadCmdList[_id].readStatus==1)
                        {
                            var data = new ReadCmd();
                            ReadCmdList.TryRemove(_id, out data);
                            return data.readResult;
                        }

                        Thread.Sleep(1);
                    }
                }
                finally
                {
                    rwlock.ExitReadLock();
                }
            }
       
        }


程序的运行逻辑改为下面的规则:


当50ms时间到后,立刻把 mr[1].Reset(); 此时后继的Read指令因为 mr[1].WaitOne() 被挂起,不允许进行计时执行。

把这一轮的read指令的ReadCmdList交给plc线程处理,然后返回plc取得结果到ReadCmdList,然后mr[1].Set(),通知后继的Read指令继续处理下一轮。


经过此次修后,貌似正常了。

不过还需要长时间验证。


《多线程读写plc内存》这个话题,如勇哥开头讲的,是一件比较困难的事。

从这个话题我们见识到并发编程带来的复杂性。



2022/11/11 勇哥注:

跑了一晚上后,发现只有最后一个地址的数值(13414)在跳动,其它的都不动,应该是死掉了。

这又是为什么呢?

image.png

还是再开一贴继续研究吧....



源代码下载:

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


支付2元,才能查看本内容!立即支付查询订单



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

作者: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