访问从不同线程访问的控件时如何处理无效的跨线程操作?

Posted

技术标签:

【中文标题】访问从不同线程访问的控件时如何处理无效的跨线程操作?【英文标题】:How to handle a Cross-thread operation not valid when accessing Controls accessed from a different thread? 【发布时间】:2021-06-28 16:14:15 【问题描述】:

我想定期检查局域网数据库连接。 简单来说,我设置了一个运行数据库检查的计时器(它每 5 秒计时一次)。 这个过程冻结了表单,所以我尝试在线程中运行这段代码,并利用 async/await 模式来解决这个问题。 使用我当前的代码,当我尝试访问控件时,出现异常:

跨线程操作无效:控制StatusStrip1 访问自 与创建它的线程不同的线程。

我发现使用Control.Invoke() 解决了这样的问题,但我不知道如何实现它。 我正在尝试访问 ToolStrip 上的 ProgressBar 和 StatusLabel。

如何解决表单冻结问题并避免异常?

我正在使用 VB.net 2019

这是我的代码:

Private Sub MyBGThread()
    If CheckConDB(ConStringDB1) Then
        TSSPBar.BackColor = Color.Green
        TSSPBar.ForeColor = Color.Green
    Else
        TSSPBar.BackColor = Color.Red
        TSSPBar.ForeColor = Color.Red
    End If
End Sub
Private Async Sub TmrDB_Tick(sender As Object, e As EventArgs) Handles TmrDB.Tick
    ' Dim thread As New Thread(AddressOf MyBGThread)
    ' thread.Start()
    Await Task.Run(Sub() MyBGThread())
End Sub

【问题讨论】:

【参考方案1】:

试试这个

Private Sub MyBGThread()
    If CheckConDB(ConStringDB1) Then
        SetTSSPBarColor(Color.Green, Color.Red)
    Else
        SetTSSPBarColor(Color.Red, Color.Green)
    End If
End Sub

Private Sub SetTSSPBarColor(ForeC As Color, BackC As Color)
    If Me.InvokeRequired Then
        Me.Invoke(Sub()
                      SetTSSPBarColor(ForeC, BackC)
                  End Sub)
    Else
        TSSPBar.ForeColor = ForeC
        TSSPBar.BackColor = BackC
    End If
End Sub

【讨论】:

【参考方案2】:

你可以这样做:

Private Async Sub TmrDB_Tick(sender As Object, e As EventArgs) Handles TmrDB.Tick
    Dim result As Boolean = Await Task.Run(Function() CheckConDB(ConStringDB1))
    TSSPBar.BackColor = If(result, Color.Green, Color.Red)
    TSSPBar.ForeColor = If(result, Color.Green, Color.Red)
End Sub

【讨论】:

干得好 Idle_Mind,非常接近我的代码,它可以工作......谢谢【参考方案3】:

如果您的同步 CheckConDB() 方法可以转换为异步方法,那么您可以更轻松地等待它的结果。如果它不能是异步的,有几个选项:

(我的建议是不要在 Timer.Tick 事件中等待,因为您不知道等待的方法需要多少时间才能完成。无论如何,计时器都会滴答作响)。

使用BeginInvoke(): 此方法发布到 UI 线程并且不会阻塞。你不需要检查InvokeRequired,这个方法可以安全地从同一个线程或工作线程中调用。

添加一个存储CancellationTokenSource的字段:

Private checkDbCts As CancellationTokenSource = Nothing

Form.Load 中(或者当您决定这样做时,但在您需要与之交互的控件创建了它们的句柄之后)运行此任务,指定调用CheckConDB() 之间的间隔;传递一个由您的 CancellationTokenSource 生成的CancellationToken:

checkDbCts = New CancellationTokenSource()
Task.Run(Function() MyBGThread(1000, bgThreadCts.Token))

任务将在每次调用CheckConDB() 之前等待[Interval] 毫秒。如果Task被取消(当CancellationTokenSource.Cancel()被调用时),它将终止并退出。

CheckConDB() 返回一个结果时,BeginInvoke() 调用ProgressUpdate 方法,根据结果的值传递一个颜色。

Private Async Function MyBGThread(interval As Integer, token As CancellationToken) As Task
    token.ThrowIfCancellationRequested()
    Try
        While True
            Await Task.Delay(interval, token)
            Dim result As Boolean = CheckConDB(ConStringDB1)
            Dim ctrlColor As Color = If(result, Color.Green, Color.Red)
            BeginInvoke(New Action(Sub() ProgressUpdate(ctrlColor)))
        End While
    Catch tce As TaskCanceledException
        Return
    End Try
End Function

使用IProgress(Of T)委托(这是首选方法):

添加一个存放delegate的Field,保留CancellationTokeSource之前的Field声明:

Private checkDbProgress As IProgress(Of Color) = Nothing

Form.Load() 中,创建一个新的Progress(Of T) 委托并启动任务,传递委托,设置为ProgressUpdate() 方法、一个Interval 和一个CancellationToken:

checkDbProgress = New Progress(Of Color)(Sub(c) ProgressUpdate(c))
checkDbCts = New CancellationTokenSource()
Task.Run(Function() MyBGThread(checkDbProgress, 1000, checkDbCts.Token))

MyBGThread() 被修改为接受 Progress(Of T) 对象。 它的Report() 方法将调用UI 线程中的ProgressUpdate() 方法,因为IProgress(Of T) 捕获了它被初始化的线程的SynchronizationContext,并使用指定的方法委托将消息异步分派到该线程。

Private Async Function MyBGThread(progress As IProgress(Of Color), interval As Integer, token As CancellationToken) As Task
    Try
        While True
            Await Task.Delay(interval, token)
            Dim result As Boolean = CheckConDB(ConStringDB1)
            Dim ctrlColor As Color = If(result, Color.Green, Color.Red)
            progress.Report(ctrlColor)
        End While
    Catch tce As TaskCanceledException
        Return
    End Try
End Function

ProgressUpdate() 方法在这两种情况下都会被调用,并且在这两种情况下它都在 UI 线程中执行:您可以在此处与 UI 元素进行交互。

Private Sub ProgressUpdate(ctrlColor As Color)
    TSSPBar.BackColor = ctrlColor
    TSSPBar.ForeColor = ctrlColor
End Sub

【讨论】:

谢谢 Jimi,你的解释很好,但我现在还不太明白,但我会稍后再试,因为我想我需要实现我的实时预览。我确定它有效

以上是关于访问从不同线程访问的控件时如何处理无效的跨线程操作?的主要内容,如果未能解决你的问题,请参考以下文章

跨线程操作无效:控件'listBox1'从一个>线程访问,而不是它在[重复]上创建的线程

跨线程操作无效:控件从创建它的线程以外的线程访问

跨线程操作无效:控件“chart1”从创建它的线程以外的线程访问

进度对话框和后台线程处于活动状态时如何处理屏幕方向更改?

进度对话框和后台线程处于活动状态时如何处理屏幕方向更改?

线程间操作无效:从不是创建控件的线程访问它的三种方法