Delphi OmniThreadLibrary + OPC 客户端

3

我正在使用单线程OPC客户端程序,管理连接到同一Siemens OPC服务器的3个不同的西门子PLC。

这个单线程的客户端程序长这样:

loop
 begin
  processPLC1;
  processPLC2;
  processPLC3;
end;

每个processPLC过程都会调用底层的OPC库,例如:
 OPCutils.WriteOPCGroupItemValue(FGroupIf, FHandleItemOpc, Value);

好的,现在我想在不同的线程中调用每个processPLC并进行并行处理。

我做了一些研究,并使用OmniThreadLibrary开始编写代码,但我认为OPC代码不是多线程安全的。 是吗?

我应该使用task.Invoke或类似的东西吗?

ReadOPC函数如何?它们返回PLC标签的值。

在这里,最佳实践是什么?

谢谢!!!

3个回答

5

我通常看到有两种方法:

1)应用程序拥有一个由单个线程拥有的单个OPC客户端实例。所有并行进程都使用某种消息传递或与拥有OPC客户端的线程同步,以读取/写入OPC值。

2)每个线程拥有自己的私有OPC客户端,每个客户端都独立地与OPC服务器通信。

就个人而言,我发现最常用的方案是前者;一个OPC(或其他专有)客户端对象,线程通过调用客户端进行同步。实际上,在几乎所有过程控制情况下,您都是为了优雅地封装逻辑真实世界任务并将其与UI隔离而进行多线程,而不是为了任何性能。这些线程可以承受相对“长”的时间来等待数据-如果需要的话,PLC将愉快地坚守阵地一百毫秒。

选择哪种方案主要取决于应用程序的规模和性质。对于许多轻量级线程,它们花费大量时间等待外部实时事件,因此在应用程序中保留单个OPC客户端实例并保存大量独立连接的开销是有意义的。对于少量重型、快速移动的OPC密集型线程,也许为每个线程提供自己的OPC客户端是有意义的。

还要记住您的OPC标签的刷新率-很多时候服务器只更新它们大约每100毫秒左右。即使只是执行单个扫描,您的PLC也可能需要至少10ms的时间。当数据永远不会刷新得那么快时,没有意义让大量线程独立地轮询服务器一百次每秒。

对于过程控制软件,您真正想要的是有大量空闲或低负载CPU时间-您的线程越轻,越好。总体系统响应性非常关键,并且在操作系统决定索引HDD的时间到来时,能够处理高负载情况(突然出现大量任务汇聚...留有余地可以让齿轮保持润滑)。大多数线程大部分时间都应该只是等待。这里通常最合理的是事件和回调。

此外,在考虑PLC编程方面也非常重要。例如,在我的机器中,我有一些非常关键的操作,每次运行时都是独特的时间限制 - 这些过程持续几分钟,计时精度在十分之一秒或更好,每天重复数百到数千次,并且每次运行的持续时间都不同。我看到这些可以通过两种方式处理 - 一种是在软件中,一种是在PLC中。对于前者,软件告诉PLC何时开始,然后一直运行,直到软件告诉它停止。这显然有明显的缺陷;在这种情况下,更好的方法是将时间间隔发送到PLC并让其进行计时。突然,所有的时间/轮询压力都从软件中消失了,进程可以优雅地处理自动化计算机崩溃等问题。在需要对OPC服务器施加重压以获取时间关键数据的情况下,通常值得重新评估整个系统的设计 - 包括软件和PLC。

感谢您提供的深入见解!不过有一件事还不太清楚:当我们谈论一个100毫秒循环时间的OPC服务器时,这个数字是仅在使用GroupAdvise回调时考虑的吗?如果我不等待标签变化回调,而是进行常量同步读写,这些函数会受到服务器循环时间的影响吗? - IgorMF
100毫秒时间通常是标签数据在服务器上的默认刷新时间 - 它可以根据每个标签进行定制,而且并非所有实现都完全相同,但通常服务器不会按扫描方式反映PLC的内容。事实上,我见过一些系统,其粗糙程度甚至比这还要高(RS-232/485在服务器<->PLC和大型标签负载之间)。例如,您可以有五个客户端每10毫秒进行同步读取,服务器将处理所有这些请求,但每个标签的值不会更快地改变其刷新率。 - J...

3

OPC基于COM技术,因此同样的规则适用。如果您想从不同的线程访问OPC服务器,则每个线程都必须自行调用CoInitialize和CoUninitialize。

这很可能与从不同进程访问OPC服务器相同。无论OPC服务器本身是单线程还是多线程都没有关系。

可能会妨碍您的是您正在使用的OPC客户端库的抽象。


基于opcconnect.com/delphi.php上找到的Simple OPC Client,我的主线程调用HR := CoInitializeSecurity(...),然后使用ServerIf := CreateComObject(ProgIDToClassID(ServerProgID)) as IOPCServer;连接到OPC服务器,然后为每个组创建组和项。现在我想并行处理三个不同的变量。在每个线程上,我应该在开头调用CoInitialize,然后在每个线程内部调用HR := ReadOPCGroupItemValue吗?我可以这样做吗,还是应该关注CriticalSection等问题?谢谢! - IgorMF
不,你必须为每个线程获取一个单独的IOPCServer实例(CoInit...,CreateCom...)。不要在线程之间共享服务器接口!想象一下你有多个客户端进程而不是多个线程会怎么做? - Uwe Raabe

2

我认为你可能过于工程化了,尽可能保持简单的做法是更好的选择,但添加线程并不能让事情更简单。

如果你有多个OPC服务器,那么使用线程可能会是更好的抽象,但现在你只需要一个连接到一个OPC服务器,那么使用多个线程和多个连接似乎过于复杂而且没有太多实际收益。

相反,使用常规的OPC订阅机制进行读取,并像以前一样按顺序写入。


我了解了!我的目标是,在“优雅地封装”每个机器作为不同的进程时,比大多数OPC客户端更具面向对象的风格。此外,由于这个客户端是GUI,我不希望任何东西阻塞屏幕和DB访问的流程。 - IgorMF
1
好的,所以这是一个GUI应用程序,那么是的,为处理OPC连接使用一个线程会很好,以免影响GUI。但我仍然要说,添加线程很容易,但后期解决同步问题会很痛苦。 - AndersK
1
我还建议使用异步订阅来确保您的客户端应用程序不会花费时间等待OPC服务器的响应。如果您有一个公寓线程应用程序,回调消息将通过Windows消息队列处理,因此它们将与UI消息整洁地交错。除非您对数据进行了大量处理,否则您不需要任何额外的线程来仅显示数据。 - Jouni Aro

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接