正确处理来自不同线程的对模块中函数的调用

本文关键字:模块 函数 调用 线程 正确处理 | 更新日期: 2023-09-27 17:59:05

我目前有一个。NET应用程序,其中函数(在窗体中)调用公共函数(位于模块中)。表单函数(WriteValueToTag)将一些值传递给模块函数(WriteToPLC),并返回True或False。模块函数(WriteToPLC)始终从WriteValueToTag调用(从未直接调用),并且仅在表单中使用。但是,表单上有一个第三方控件,它在调用WriteValueToTag函数时引发事件。因此,它是在一个独立于主UI窗体的线程上调用的

我遇到的问题是(根据我的日志记录)这个函数有时似乎是从不同的线程同时调用的。参数值出现不同步导致错误。这两个函数都不更新表单上的任何数据或控件(所以我不检查Invoke.Required等)。我不确定是应该在函数中进行此检查,还是应该使用thread在自己的线程中启动WriteToPLC函数。开始)?我曾尝试在WriteToPLC模块函数的整个内容周围放置SyncLock,但这似乎没有帮助。

如何处理模块中接收参数的公共函数的调用,这些参数可以在任何时候从不同的线程调用,以便它为每个调用它的线程唯一地运行?任何示例或链接都会有所帮助。非常感谢。

模块中的函数声明如下:

Public Function WriteToPLC(ByRef PLC As Logix.Controller, ByRef PLCTag As Logix.Tag) As Boolean

调用它的形式中的子例程声明是:

Private Function WriteValueToTag(ByVal PLCTagValue As Object, ByRef PLCTagName As Logix.Tag) As Boolean

在这里,WriteToPLC被称为传递适当的值。

我想确保在使用Invoke时保留ByRef参数,所以我不确定WriteValueToTag函数的以下代码是否正确:

Delegate Function DelegateWriteValueToTag(ByVal PLCTagValue As Object, ByRef PLCTagName As Logix.Tag)
Private Function WriteValueToTag(ByVal PLCTagValue As Object, ByRef PLCTagName As Logix.Tag) As Boolean
    ''Dim writeLock As New Object
    Dim tagName As Logix.Tag = PLCTagName 'remember PLCTagName since it's a ByRef param
    If (InvokeRequired) Then
        Dim newDelegate As New DelegateWriteValueToTag(AddressOf WriteValueToTag)
        Dim eventArgs() As Object = {PLCTagValue, tagName}
        Log.Debug("Calling Function WriteValueToTag() via InvokeRequired")
        Dim result As Boolean
        result = CType(Invoke(newDelegate, eventArgs), Boolean)
        PLCTagName.Value = tagName.Value 'put value back in since PLCTagName param is passed ByRef
        Return result
    Else
        Try
            Log.Debug("Entering Function WriteValueToTag()")
            Log.Debug("  writing value {0} to tag {1}", PLCTagValue, tagName.Name)
            PLCTagName.Value = PLCTagValue
            Return(WriteToPLC(mPLC, PLCTagName))
        Catch ex As Exception
            HandleError(ex, False)
            Return False
        End Try
    End If
End Function

这是调用WriteValueToTag以保留ByRef参数的正确方法吗?我想知道,由于我使用"eventArgs()"来传递变量,这是一个对象,这是否会传递对该对象的引用(即使Invoke的参数声明为ByVal)。如果这是真的,我是否仍然需要声明任何局部变量tagName来存储该值,然后在Invoke之后将其设置回?

还想知道它是否会像这样简单:

 If (InvokeRequired) Then
        Dim newDelegate As New DelegateWriteValueToTag(AddressOf WriteValueToTag)
        Dim eventArgs() As Object = {PLCTagValue, PLCTagName}
        Log.Debug("Calling Function WriteValueToTag() via InvokeRequired")
        Dim result As Boolean
        result = CType(Invoke(newDelegate, eventArgs), Boolean)
        PLCTagName.Value = PLCTagValue 'retain PLCTagName.Value as it's passed ByRef and Invoke will not change it
        Return result

正确处理来自不同线程的对模块中函数的调用

我不清楚这个问题是如何成为C#问题的。显示的小代码似乎是VB.NET代码。但是,这里有一个C#版本的答案…

有多种方法可以确保库方法一次只能由一个线程调用。坦率地说,由于缺乏一个好的、最小的complete代码示例来准确地显示您的场景是如何工作的,因此无法确定最佳方法是什么

但在Windows窗体程序中,最常见的(在某些情况下也是唯一正确的)方法是简单地确保所有调用都是使用主UI线程进行的。这是使用Control.Invoke()方法实现的。

如果没有一个好的代码示例,就不可能知道代码的实际外观。但基本的想法是修改WriteValueToTag()方法,使其看起来像这样:

private void WriteValueToTag(object PLCTagValue, ref Logix.Tag PLCTagName)
{
    Logix.Tag tagName = PLCTagName;
    this.Invoke((MethodInvoker)(() => WriteValueToTagImpl(PLCTagValue, ref tagName)));
    PLCTagName = tagName;
}
private void WriteValueToTagImpl(object PLCTagValue, ref Logix.Tag PLCTagName)
{
    // original method body here
}

当然,您可以在一个命名方法中实现所有这些,将"…Impl"方法体放入原始方法中的匿名方法中。两者都可以。

请注意,如果将原始方法体放入匿名方法中,则必须在该方法体中使用tagName而不是PLCTagName,因为不能在匿名方法体中通过引用参数(refout)来使用(这就是为什么在调用Invoke()之前必须将参数复制到本地,然后在调用返回时将本地复制回参数)。

这里的主要问题是,无论谁调用该方法,该方法的实际工作都将始终在主UI线程中执行,从而确保该方法一次只能由单个线程执行,并且在主线程中执行(以防部分或全部困难实际上是"线程亲和性"问题,而不是简单的同步问题)。


编辑:
在回答您关于VB.NET的问题时,请不要使用…我觉得你尝试的代码不起作用。它在语义上肯定与上述内容不等价,除非传递引用是多余的,否则它不会实现正确的结果。

还要注意,使用InvokeRequired属性是IMHO不正确和无用的。这只会使代码变得多余。相反,只要始终调用Invoke()即可。它会根据你所在的线程做正确的事情。有关这一点的更多信息,请参阅MSDN使用Control的规范技术。Invoke是蹩脚的。

这里有一个VB.NET实现,其结构与我上面的示例相同,但使用您自己的方法体作为起点:

Private Function WriteValueToTag(ByVal PLCTagValue As Object, ByRef PLCTagName As Logix.Tag) As Boolean
    Dim tagName As Logix.Tag = PLCTagName 'remember PLCTagName since it's a ByRef param
    Dim result As Boolean
    Invoke(CType(
    (
        Sub()
            Try
                Log.Debug("Entering Function WriteValueToTag()")
                Log.Debug("  writing value {0} to tag {1}", PLCTagValue, tagName.Name)
                tagName.Value = PLCTagValue
                result = WriteToPLC(mPLC, tagName)
            Catch ex As Exception
                HandleError(ex, False)
                result = False
            End Try
        End Sub
    ), MethodInvoker))
    PLCTagName = tagName
    Return result
End Function

调用Control.Invoke()时可以直接通过引用参数进行处理。为此,您需要从传递给Invoke()方法的原始args数组中检索更新的值;元素值将根据被调用方法所做的任何修改来更新。但我更倾向于以上述方式处理它们。我发现这种语法更清晰、更容易推广,其中被调用的委托总是一个无参数的void方法(即Sub()),并且返回值和通过引用参数的处理被放入匿名方法体本身。