【问题标题】:Correctly handle calls to a function in a module from different threads?正确处理来自不同线程的模块中函数的调用?
【发布时间】:2015-06-23 14:03:27
【问题描述】:

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

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

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

模块中的函数声明如下:

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

调用 this 的子程序声明如下:

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

...

【问题讨论】:

  • 我感觉问题不在于“参数混淆”(每个线程都有自己的调用堆栈来跟踪传递给方法的参数)。问题可能出在 WriteToPLC 本身 - 无论它在做什么,一次应该只有一个线程在做。
  • @mbeckish - 同意。那么,我怎样才能确保 WriteToPLC 只为每个调用它的线程做它应该做的事情呢?
  • 由于您没有展示 WriteToPLC 所做的事情或具体解释它做错了什么,因此无法说...
  • 万一,你在哪里打电话给Application.DoEvents()?或者您是否在WriteValueToTag 内做任何会直接或间接导致第 3 方控制触发的事情?是什么让您认为涉及的线程不止一个?您可以通过设置断点并在 Visual Studio 中查看线程列表来验证吗?
  • 我会实现Producer / Consumer pattern。调用“WriteToPLC”的线程应该将请求对象放入队列中。只有 1 个消费者线程处理请求并对 WriteToPLC 进行实际调用,一次一个。

标签: c# .net vb.net multithreading


【解决方案1】:

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

有多种方法可以确保库方法一次仅由一个线程调用。坦率地说,由于缺少 a good, minimal, complete code example 来准确显示您的场景是如何工作的,因此无法确定最好的方法是什么。

但在 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 属性是不正确且无用的。它只是使代码变得多余。相反,请始终致电Invoke()。它会根据您所在的线程做正确的事情。有关这一点的更多信息,请参阅MSDN’s canonical technique for using Control.Invoke is lame

这是一个 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()),并且返回值和引用参数的处理被放入匿名方法体本身,更清晰,更容易概括。

【讨论】:

  • 我想我可以将 C# 转换为 VB.NET,但这可能有点不同。我已经更新了我的原始帖子以显示我的整个 WriteValueToTag 函数。我的代码在使用 Invoke 方法时处理传入的 ByRef 参数是否正确?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-07-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多