当前位置:首页 > C#编程 > C#/.net框架 > 正文内容

windows下c#程序提高实时性

Jorge2年前 (2022-05-08)C#/.net框架1034

引言:我们手里的运控软件对实时性有了新要求。

因为同设备上运行的其它视觉软件对cpu时间有变态的占用,压缩了运控软件的cpu占用时间。

我们知道如果视觉软件卡一下,最多是处理时间长一点,但是运控软件卡一下,那就意味着撞机的问题,这个要严重得多。



这个问题会持续把研究结果更新到本贴子。


(一)提升线程和进程的优先级别


ProcessPriorityClass等级说明

public enum ProcessPriorityClass


字段

AboveNormal32768

指定进程的优先级高于 Normal 但低于 High。


BelowNormal16384

指定进程的优先级在 Idle 之上,但在 Normal 之下。


High128

指定进程执行必须立即执行的时间关键任务,如 Task List 对话框,不管操作系统的负荷如何,用户调用该对话框后均必须迅速响应。 该进程的线程优先于普通或空闲优先级类进程的线程。

为进程的优先级类指定 High 这一个优先级时需谨慎,因为高优先级类应用程序几乎可以使用所有可用的处理器时间。


Idle64

指定此进程的线程只能在系统空闲时运行,如屏幕保护程序。 更高优先级类中运行的任何进程的线程都优先于此进程的线程。 此优先级类由子进程继承。


Normal32

指定进程没有特殊的安排需求。


RealTime256

指定进程拥有可能的最高优先级。

具有 RealTime 优先级的进程的线程抢占所有其他进程的线程,包括执行重要任务的操作系统进程。 因此,执行时间并不太短的 RealTime 优先级进程可能导致磁盘高速缓存不刷新或鼠标无响应。


设置优先级:


//设置当前程序的进程优先级

           var prop = System.Diagnostics.Process.GetCurrentProcess();

           prop.PriorityClass = System.Diagnostics.ProcessPriorityClass.RealTime;

任务管理器查看进程优先级




下面引用一篇博文,提到马上要谈到的第二点“多媒体定时器”

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

C#
此文是在实现modbus-RTU通讯的基础上总结而来的,主要讲述了如何提高window做工业控制时的实时性能。
PC机上做控制系统,一般就是在window或者linux操作系统上做控制系统软件开发,window上做控制系统,有如下优势:1)现有的设备驱动支持2)各类厂家提供的现成的卡板3)以及比较平民化的开发平台4)数量众多的开发人员。2 提高实时性手段
window是一个非实时操作系统,如果要做实时任务的话,有必要用些特别的手段,来提高其实时控制的能力。1)    语言最好选择C语言,之所以选择C语言,因为工业控制主要基于window API编程,使用很多window内核的服务。2)    多任务一般采用多线程来实现,根据任务的紧要程度,设置线程的优先级别,一定要设置优先级。3)    如果在一个线程中有多个任务的话,建议采用协程的方式实现多任务,任务内部最好不要调用Sleep()等休眠函数4)    为了提高内核的时钟的调度精度,必须使用多媒体定时器并且将其精度设置为1ms,这样内核任务调度精度也提高为1ms,
这估计是微软的内部耦合。如果不使用多媒体定时器的话,内核定时器时钟精度一般只有15ms左右。5)    每个线程的一个循环必须释放一次CPU,采用Sleep(1),这样才能保证机器的CPU不被耗光,
否则低优先级的任务就没有机会得到执行了。6)计时采用QueryPerformanceFrequency()和 QueryPerformanceCounter()函数来计时,可以达到ns级别。
上面手段都是用户空间的手段,在内核空间应该也有手段(应用程序作为驱动跑),但是我不熟,就不说了。3 效果
使用以上几种方式实现的控制程序,时间关键优先级别多线程任务调度精度可以达到2-5ms级别,
计时精度可以达到ns级别。这对于很多(大部分)要求不高的控制来说,基本是够用的。
至于驱动部分,window的驱动的反应速度还是挺快的(CPU占用率不过高的情况下面达到us级别的反应),
这个和我们用户空间的程序不一样,例如window的串口驱动,可以将接收FIFO设置为1,
采用事件方式读串口,即使波特率为115200bps,也不会丢失数据。
使用上面的方法,我在PC机上实现了modbus-RTU客户端,其3.5T设置为5ms,可以很稳定地和PLC主设备通讯。4 硬实时
如果在PC机上追求真正的硬实时,那么一般是在window或者linux系统中加入硬实时内核,形成双内核系统,
例如RTX或者RTAI,这些都有成功的案例,例如西门子的基于RTX的数控系统,基于RTAI的实时控制系统,
这些实时系统都是用于100us级别的硬实时系统中,需要开发者自己写驱动,其架构是实时内核上跑实时任务,
普通内核上跑一般任务,实时任务和一般任务间通过通讯的方式沟通。由于我还没有在这些实时系统上做过开发,所以暂时不写。


(二)多媒体定时器


在工业生产控制系统中,有许多需要定时完成的操作,如数据采集程序。Win32提供了一个基于消息机制的定时器,使用SetTimer函数创建一个内存对象,设定间隔时间,当到达要求的间隔时,计时器对象发送一个WM_TIMER消息,由相应函数处理。但是由于WM_TIMER优先级低,只有等待消息队列中的其他消息都处理完毕后系统才会响应该消息。而且消息队列中的多个WM_TIMER会被合并,因此Win32定时器的精度低,不能满足工业实时控制系统的要求。


本文将介绍一种精度较高的多媒体定时器,该定时器并不依赖于消息机制,可以实现1ms的定时精度。由于多媒体定时器另外开辟一个独立线程执行定时器回调函数,因此当回调函数耗时较多时并不会导致UI的“假死”,相对而言,Win32定时器隶属于主线程,一旦定时器回调函数耗时较多,就会导致UI的“假死”。


几个拉跨的定时器精度

select选择模型:15ms

Sleep(1) :15ms

timeGetTime: 5ms

QueryPerformanceCounter:<1微秒 (win2000支持)


精度达到1ms的定时器:多媒体定时器(win95支持)

原理分析:多媒体定时器被创建后,应用程序有一个定时器消息队列,系统以1ms(最高精度)的周期向程序发送定时器消息。当应用程序收到消息时,调用回调函数。此回调函数优先级非常高(最高),甚至可以将Sleep的精度压缩到1ms(通过屏蔽Sleep的方法)(普通线程为15ms,即便是将优先级拉到最高)。当回调函数的执行时间大于1ms (内部有如Sleep(1000))或者长耗时循环操作,会导致定时器消息积压,当挤压消失,系统会在1ms内多次(最高速度)调用回调函数 t<1ms,直到定时器消息队列为空,然后恢复1ms周期。


此定时器C++演示代码见:https://blog.csdn.net/u010839382/article/details/52238255


下面勇哥提供C#的演示代码:

C#
using System;using System.Runtime.InteropServices;namespace HighPrecisionTimer{
    /// <summary>
    /// 定时器分辨率:毫秒
    /// </summary>
    struct TimerCaps    {
        /// <summary>最小周期</summary>
        public int periodMin;
        /// <summary>最大周期</summary>
        public int periodMax;
    }

    /// <summary>
    /// 高精度定时器
    /// </summary>
    public class HPTimer
    {
        static HPTimer()
        {
            TimeGetDevCaps(ref _caps, Marshal.SizeOf(_caps));
        }

        public HPTimer()
        {
            Running = false;
            _interval = _caps.periodMin;
            _resolution = _caps.periodMin;
            _callback = new TimerCallback(TimerEventCallback);
        }

        ~HPTimer()
        {
            TimeKillEvent(_id);
        }

        /// <summary>
        /// 系统定时器回调
        /// </summary>
        /// <param name="id">定时器编号</param>
        /// <param name="msg">预留,不使用</param>
        /// <param name="user">用户实例数据</param>
        /// <param name="param1">预留,不使用</param>
        /// <param name="param2">预留,不使用</param>
        private delegate void TimerCallback(int id, int msg, int user, int param1, int param2);

        #region 动态库接口

        /// <summary>
        /// 查询设备支持的定时器分辨率
        /// </summary>
        /// <param name="ptc">定时器分辨率结构体指针</param>
        /// <param name="cbtc">定时器分辨率结构体大小</param>
        /// <returns></returns>
        [DllImport("winmm.dll", EntryPoint = "timeGetDevCaps")]
        private static extern TimerError TimeGetDevCaps(ref TimerCaps ptc, int cbtc);

        /// <summary>
        /// 绑定定时器事件
        /// </summary>
        /// <param name="delay">延时:毫秒</param>
        /// <param name="resolution">分辨率</param>
        /// <param name="callback">回调接口</param>
        /// <param name="user">用户提供的回调数据</param>
        /// <param name="eventType"></param>
        [DllImport("winmm.dll", EntryPoint = "timeSetEvent")]
        private static extern int TimeSetEvent(int delay, int resolution, TimerCallback callback, int user, int eventType);

        /// <summary>
        /// 终止定时器
        /// </summary>
        /// <param name="id">定时器编号</param>
        [DllImport("winmm.dll", EntryPoint = "timeKillEvent")]
        private static extern TimerError TimeKillEvent(int id);

        #endregion

        #region 属性

        /// <summary>时间间隔:毫秒</summary>
        public int Interval        {
            get { return _interval; }
            set
            {
                if (value < _caps.periodMin || value > _caps.periodMax)
                    throw new Exception("无效的计时间隔");
                _interval = value;
            }
        }

        public bool Running { get; private set; }

        #endregion

        #region 事件

        public event Action Ticked;

        #endregion

        #region 公开方法

        public void Start()
        {
            if (!Running)
            {
                _id = TimeSetEvent(_interval, _resolution, _callback, 0,
                    (int)EventType01.TIME_PERIODIC | (int)EventType02.TIME_KILL_SYNCHRONOUS);
                if (_id == 0) throw new Exception("启动定时器失败");
                Running = true;
            }
        }

        public void Stop()
        {
            if (Running)
            {
                TimeKillEvent(_id);
                Running = false;
            }
        }

        #endregion

        #region 内部方法

        private void TimerEventCallback(int id, int msg, int user, int param1, int param2)
        {
            Ticked?.Invoke();
        }

        #endregion

        #region 字段

        // 系统定时器分辨率
        private static TimerCaps _caps;
        // 定时器间隔
        private int _interval;
        // 定时器分辨率
        private int _resolution;
        // 定时器回调
        private TimerCallback _callback;
        // 定时器编号
        private int _id;

        #endregion
    }}


调用方法如下:

C#
static readonly HPTimer timer = new HPTimer();// 每1毫秒执行一次timer.Interval =1;timer.Ticked += Timer_Ticked;timer.Start();


注:

把此多媒体定时器程序编制成一个控制台程序,隐藏窗体。然后把进程的级别提高到最高级别“实时”

这样可以实现实时精度为1ms的动作,可以用于IO扫描任务。

这样无论视觉程序有没有卡顿、cpu占用率是不是极高,都不会影响到此定时器的回调周期。

所以在工业设备软件里,最好不要把IO扫描放到主程序里用线程来执行,而是应该按勇哥所说的放在另一个独立的进程里单独执行,并且要保证其有一定精度的实时性,这样就不受其它进程的卡顿的影响。


(三)其它的实时性提升办法


简单的说:

一是编制windows驱动程序(内核级别的应用),这个比用户空间的程序响应快几个数量级,可以达到us,但是这个勇哥实在没那个能力。

二是锁定物理内存,这样可以避免windows使用虚拟内存进行页面交换带来的效率下降,此方面的资料百度很少。


下面引用一下相关的论文,如果作者认为侵权请联系勇哥删除。

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png

image.png


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

作者:hackpig

来源:www.skcircle.com

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


扫描二维码推送至手机访问。

版权声明:本文由7点博客发布,如需转载请注明出处。

本文链接:http://6dot.cn/?id=35

标签: .NET.NET框架
分享给朋友:
返回列表

没有更早的文章了...

下一篇:C# 控件闪烁问题的解决

“windows下c#程序提高实时性” 的相关文章

C#的变迁史 - C# 4.0 之线程安全集合篇

C#的变迁史 - C# 4.0 之线程安全集合篇

作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了。  而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们,微软为我们带来了强大的并行集合:System.Collections.Concurrent里面的各...

C#字符串与享元(Flyweight)模式

C#字符串与享元(Flyweight)模式

注:关注这个话题是因为看到C#的关键字 lock时,其传入引用对象。因为string也是引用对象,所以能否做为lock的参数?对于这个问题,要搞明白C#的字符串的一个特点,它使用类似于享元模式的机制。因此在lock中锁字符串是相当不安全的。下面贴子是对C#字符串与享元模式的深入讨论。写这个文章,主要...

C# Modelbus crc16计算校验和程序

C# Modelbus crc16计算校验和程序

我们手里一个无刷电机,采用485的modelbus crc16协议来控制。因此需要一个计算校验和的工具。源码:C#using System;using System.Collections.Generic;using System.ComponentModel;usin...

C#测量程序运行时间及cpu使用时间

C#测量程序运行时间及cpu使用时间

对一个服务器程序想统计每秒可以处理多少数据包,要如何做?答案是用处理数据包的总数,除以累记处理数据包用的时间。这里要指出的是, 运行一段程序,使用的cpu时间,跟实际运行的时间是不一样的。附例如下:C#private void ShowRunTime() {...

计算代码执行时间,可以精确到十亿分之一秒

计算代码执行时间,可以精确到十亿分之一秒

注:.Net的Stopwatch类可以精确到1/10000毫秒, 有没有更精确的计时吗?见下面的代码。暂时没试过效果,大家可以试下。计算某个代码片段的执行时间,精确到CPU执行一条指令所用的时间(十亿分之一秒),可用于精确计算某个算法的执行时间。 代码:C#using Syste...

C# 当前不会命中断点 还没有为该文档加载任何符号

C# 当前不会命中断点 还没有为该文档加载任何符号

这个问题网上的经验大概如下:1。 清空方案,重新编译2。 删除项目bin目录下的东西,重新编译3。 解决相互引用的问题。4。 确保不是run的release5。把项目编译改为x866。 好像没发现其它的了。。。这些解决不了我们手上的项目的问题。我们的工程有几十个项目。我长话短说,解决方法是:引导项目...

发表评论

访客

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