引言: 勇哥目前需要在两个进程间进行线程同步,要求是这样的: 进程A负责扫描板卡IO,并将结果写入共享内存。 进程B负责读取共享内存的结果。 这个时候需要在进程A写入IO扫描结果后立刻通知进程B读取结果。进程A的写入动作节拍是4ms, 进程A和进程B必须在在这4ms节拍内按先后次序完成数据的写入与读取。 这是由于io扫描的结果除了io的电平状态,还包括上升沿和下降沿的结果, 如果两个进程的操作不在同一个4ms节拍内顺序完成的话, 取得的上升沿和下降沿状态就不是同步的。 为达到这种进程间同步的效果,先做一点知识梳理,下面引用一批网络上的文章,以供研究。
要实现我所想需要解决下面几个问题:
读进程和写进程在同一个节拍中
在同一节拍中时,先写后读,且两者完成时间保证在同一个节拍长度内
两个进程间共享消息的办法
对于问题3,勇哥使用共享内存来解决。
使用共享内存要注意: 对同一个地址写和读不要同时进行、要严格控制多线程访问的情况。
勇哥说的节拍是什么呢?
可以参考下图关于上升沿和下降沿的说明。
其中红色段就是一个扫描周期,蓝色段是第二个扫描周期。
扫描周期是固定的,例如每2ms一次。
一个扫描周期就是勇哥指的一个节拍。
写进程和读进程的工作必须在同一个节拍内按顺序完成。
上升沿和下降沿的判定则需要把当前扫描周期的电平和上一个扫描周期电平进行对比,它是在相连的两个扫描周期中进行判定的。
对于节拍,可以考虑用定时器实现,一般我们常见的有三类用法:
注意,ui控件中那个定时器是不可用的,它只是用于ui界面改新信息。
对于第一种,我们使用死循环的线程,然后sleep来控制节拍,可以吗?
答案是不可以。
看下面的例子,tfun()是一个线程,每次你查看d1变量,会发现时间都不同,但是肯定会远远超过1。
这是因为windows是抢占式多任务系统,sleep的精度是由操作系统按硬件资源和当前系统的状态自由发挥出来的。
那么使用上面说的后两种,Timer和Thread类的定时器可不可以呢?
答案也是不可以,还是精度不能保证。
勇哥这里使用的是多媒体定时器,最高精度可以保证1ms,只有几十个ns的误差。
使得它可以很好的实现我们需要的节拍。
精度达到1ms的定时器:多媒体定时器(win95支持) 原理分析:多媒体定时器被创建后,应用程序有一个定时器消息队列, 系统以1ms(最高精度)的周期向程序发送定时器消息。 当应用程序收到消息时,调用回调函数。此回调函数优先级非常高(最高), 甚至可以将Sleep的精度压缩到1ms(通过屏蔽Sleep的方法) (普通线程为15ms,即便是将优先级拉到最高)。 当回调函数的执行时间大于1ms (内部有如Sleep(1000))或者长耗时循环操作, 会导致定时器消息积压,当挤压消失,系统会在1ms内多次(最高速度) 调用回调函数 t<1ms,直到定时器消息队列为空,然后恢复1ms周期。
对于同一节拍中怎么控制先写后读的顺序,勇哥使用共享内存互斥来完成。
演示代码分为写部分和读部分,各自是一个独立的控制台进程。
写进程的循环周期为2ms,读进程的为1ms。
下面源码是写的部分:
using Samsun.Domain.MotionCard.CIOProcess; using Samsun.Domain.MotionCard.Common; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace tim1 { //write class Program { static CVARreadwriteIOTable mem = new CVARreadwriteIOTable(); static readonly HPTimer timer = new HPTimer(); //DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff:ffffff")) static void Main(string[] args) { mem.AddOrUpdateVR(2, "0"); int interTime = 2; timer.Interval = interTime; timer.Ticked += Timer_Ticked; mem.AddOrUpdateVR(7, "0"); mem.AddOrUpdateVR(8, "100"); while (true) { if (mem.ReadMemVR(7).CurrentValue.ToString() == "100") { timer.Start(); break; } } while (true) { Thread.Sleep(2); } } static List<string> buff = new List<string>(); static int js1 = 0; private static void Timer_Ticked() { //write if (mem.ReadMemVR(2).CurrentValue.ToString() == "0") { var t1 = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff:ffffff"); mem.AddOrUpdateVR(1, t1); buff.Add(t1+"\r\n"); mem.AddOrUpdateVR(2, "100"); } if(++js1>1000*10/2) { var sb1 = new StringBuilder(); for(int i=0;i<buff.Count;i++) { sb1.Append(buff[i]); } File.AppendAllText("d:\\write.txt", sb1.ToString()+"\r\n*********"+ js1.ToString()); timer.Stop(); } } } }
下面源码是读的部分:
using Samsun.Domain.MotionCard.CIOProcess; using Samsun.Domain.MotionCard.Common; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Text; using System.Threading; using System.Threading.Tasks; namespace time2 { //read class Program { static CVARreadwriteIOTable mem = new CVARreadwriteIOTable(); static readonly HPTimer timer = new HPTimer(); static Thread t1 = null; static void Main(string[] args) { //Stopwatch sw1 = new Stopwatch();sw1.Start(); //while(true) //{ // if(sw1.ElapsedMilliseconds>1000) // { // sw1.Restart(); // Console.WriteLine($"{mem.ReadMemVR(1).CurrentValue.ToString()}"); // } //} int interTime = 1; timer.Interval = interTime; timer.Ticked += Timer_Ticked; //t1 = new Thread(new ThreadStart(tfun)); //t1.IsBackground = true; //t1.Start(); //mem.AddOrUpdateVR(8, "0"); //通过共享内存7, 8让read,write的定时器同时启动,即保证起始节拍一致 mem.AddOrUpdateVR(7, "100"); while (true) { if (mem.ReadMemVR(8).CurrentValue.ToString() == "100") { timer.Start(); break; } } while (true) { Thread.Sleep(2); } } static int js1 = 0; static int js2 = 0; static List<string> buff = new List<string>(); private static void tfun() { Stopwatch sw1 = new Stopwatch(); sw1.Start(); while (true) { sw1.Restart(); Thread.Sleep(1); sw1.Stop(); var d1 = sw1.Elapsed.TotalMilliseconds; if (mem.ReadMemVR(2).CurrentValue.ToString() == "100") { buff.Add($"{mem.ReadMemVR(1).CurrentValue.ToString()}\r\n"); mem.AddOrUpdateVR(2, "0"); } if (sw1.ElapsedMilliseconds > 10 * 1000) break; } var sb1 = new StringBuilder(); for (int i = 0; i < buff.Count; i++) { sb1.Append(buff[i]); } File.AppendAllText("e:\\read.txt", sb1.ToString()); } //read private static void Timer_Ticked() { //通过共享内存2来保证同一个节拍内先写后读 if (mem.ReadMemVR(2).CurrentValue.ToString() == "100") { buff.Add($"{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff:ffffff")}-- {mem.ReadMemVR(1).CurrentValue.ToString()}\r\n"); mem.AddOrUpdateVR(2, "0"); } if (++js1 > 1000 * 10 ) { var sb1 = new StringBuilder(); for (int i = 0; i < buff.Count; i++) { sb1.Append(buff[i]); } File.AppendAllText("d:\\read.txt", sb1.ToString() + "\r\n*********" + js1.ToString()); timer.Stop(); } //if (js1 == 0) //{ // ++js1; // mem.AddOrUpdateVR(5, t1); //5 read init data //} //++js2; //if(js2>500) //{ // js2 = 0; // Console.WriteLine($"write: {mem.ReadMemVR(4).CurrentValue.ToString()}"); // Console.WriteLine($"read: {mem.ReadMemVR(5).CurrentValue.ToString()}"); //} } } }
上面源码中,为了保证两个进程的起始节拍一致,用共享内存控制了同时启动timer.Start()
在下面结果图中,勇哥以颜色标记了读写对。
可以看到读写顺序,读写节拍都是正确的。
请注意读的数据中,--前面记录的是读的当前时间,后面是写的时候的时间(共享内存记录的写的时间)
另外注意因为write循环周期是2ms, read是1ms,所以定时器回调计数一个是5001, 一个是10001。
而如果你数一下记录的最后一行,可以看到行数是一致的,这说明内部记录的buff是对应关系。
上升沿和下降沿只有在节拍和读写顺序控制都满足的情况下,结果才是正确的。
对于上升沿下降沿概念不清楚的童鞋,勇哥再解释一下:
在plc中: 上升沿:从0变1的一瞬间,接通一个扫描周期 下降沿:从1变0的一瞬间,接通一个扫描周期 对于io板卡、运动卡来讲,一般是默认电平为高电平,即低电平有效。所以要变化成如下: 上升沿:从1变0的一瞬间 下降沿:从0变1的一瞬间
得到源码后,请先执行write, 再执行read,启动的先后次序如果不同程序流程会卡住。
源码工程如下图所示:
其中write进程是tim1, time2是read进程,HPTimer是多媒体定时器,CVARreadwriteIOTable为共享内存应用类。
勇哥对本文做了一个视频讲解,各位可以参考:
B站视频如下:
本站收费下载:
扫码收费10元,勇哥用以支付本站服务器费用。

