由于近期在研究防止窗体假死该如何给长时间运行的方法加一个等待的画面,需要用到异步委托和Lambda 表达式,所以紧急饿补了相关内容,现把学习感受记录备查。
相关博文:《异步委托实现等待窗体(loading界面),执行任务超时可以取消操作》
本文主要是简化《程序长时间执行任务时窗体会失去响应造成假死避免重复点击按钮的解决思路》一文代码,更突出问题。
一、窗体假死
新建一个windows窗体应用程序,在窗体里拉入一个按钮Button和一个Label控件,为按钮单击事件添加如下代码:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'******************************************
'模拟长时间工作代码
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
'******************************************
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
End Sub
执行循环时窗体不能拖动,点击按钮无反应,Label也不能实时显示工作进度,直到循环结束直接显示100%,窗体处于假死状态。
二、同步委托
为了便于理解,我把长时间运行的代码封装到一个方法里,然后在按钮单击事件里调用该方法。
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'******************************************
'调用长时间工作代码
Call aa()
'******************************************
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
End Sub
Private Sub aa()
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
Me.Label1.Text = i.ToString & "%"
Next
End Sub
改用同步委托(Delegate.Invoke)调用方法:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'******************************************
'调用长时间工作代码
'Call aa()
'改用委托调用方法
Dim method As MethodInvoker = New MethodInvoker(AddressOf aa)
method.Invoke
'******************************************
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
End Sub
这里使用了程序内置的委托类型MethodInvoker,可以省去声明定义委托的麻烦,运行代码发现Delegate.Invoke方法与直接调用方法是一样的,没有任何区别。为何要用委托呢?这里只是为了说明问题,用和不用都是一样的。
三、异步委托解决窗体假死
把method.Invoke代码改成method.BeginInvoke(Nothing, Nothing),执行代码,会弹出一个错误提示:“System.InvalidOperationException:“线程间操作无效: 从不是创建控件“Label1”的线程访问它。”如图:
解决办法之一是在构造函数或者窗体Load的代码里添加下面一句:
Control.CheckForIllegalCrossThreadCalls = False
这是不安全的线程操作,容易造成死锁,所以并不建议这样处理,推荐使用Control.Invoke或Control.BeginInvoke方法。完整代码如下所示:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'******************************************
'调用长时间工作代码
'Call aa()
'改用委托调用方法
Dim method As MethodInvoker = New MethodInvoker(AddressOf aa)
'method.Invoke
'改用异步委托
method.BeginInvoke(Nothing, Nothing)
'******************************************
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
End Sub
Private Sub aa()
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
'Me.Label1.Text = i.ToString & "%"
'修改为
Dim Actiondelegate = New Action(Of Integer)(AddressOf bb)
Me.Label1.Invoke(Actiondelegate, i)
Next
End Sub
Private Sub bb(ByVal int As Integer)
'显示工作进度
Me.Label1.Text = int.ToString & "%"
End Sub
你也可以把Me.Label1.Invoke(Actiondelegate, i)修改为Me.Label1.BeginInvoke(Actiondelegate, i)进行测试。
四、Delegate.Invoke、Delegate.BeginInvoke 与Control.Invoke、Control.BeginInvoke
通过以上代码运行测试,相信你对Delegate.Invoke、Delegate.BeginInvoke 、Control.Invoke、Control.BeginInvoke有了一定的认识,下面内容摘抄自《Delegate.Invoke、Delegate.BeginInvoke 与Control.Invoke、Control.BeginInvoke》一文。
1、Delegate.Invoke (委托同步调用)
a、委托的Invoke方法,在当前线程中执行委托。
b、委托执行时阻塞当前线程,知道委托执行完毕,当前线程才继续向下执行。
c、委托的Invoke方法,类似方法的常规调用。
2、Delegate.BeginInvoke (委托异步调用)
a、委托的BeginInvoke方法,在线程池分配的子线程中执行委托
b、委托执行时不会阻塞主线程(调用委托的BeginInvoke线程),主线程继续向下执行。
c、委托执行时会阻塞子线程。
d、委托结束时,如果有返回值,子线程讲返回值传递给主线程;如果有回调函数,子线程将继续执行回调函数。
3、Control.Invoke (同步调用)
(1)在主线程(UI线程)中调用Control.Invoke
a、在主线程(UI线程)中调用Control.Invoke,先执行Invoke的方法,再执行Invoke后面的代码。
(2)在子线程中调用Control.Invoke
a、子线程中调用Control.Invoke,子线程将调用的方法封装成消息,调用API的RegisterWindowMessage()向UI窗口发送消息。 主线程继续向下执行,子线程处于阻塞状态。
b、当该消息被主线程执行完成后,子线程才能继续往下执行。
4、Control.BeginInvoke (异步调用)
(1)在主线程(UI线程)中调用Control.BeginInvoke
a、在主线程(UI线程)中调用Control.BeginInvoke,将调用的方法封装成消息,调用API的RegisterWindowMessage()向UI窗口发送消息。先执行Invoke后面的代码,再执行Invoke的方法。
(2)在子线程中调用Control.BeginInvoke
a、子线程中调用Control.BeginInvoke,子线程将调用的方法封装成消息,调用API的RegisterWindowMessage()向UI窗口发送消息。主线程继续向下执行,子线程也继续向下执行。
b、最后由主线程执行Invoke的方法。
可见Control.Invoke、Control.BeginInvoke都工作在UI线程。
如果想了解更多相关知识,博客园中横竖都溢的三篇文章写的不错:
1、谈.Net委托与线程——创建无阻塞的异步调用(一)
2、谈.Net委托与线程——创建无阻塞的异步调用(二)
3、谈.Net委托与线程——解决窗体假死
官方帮助文档:
1、委托 (Visual Basic)
2、使用异步方式调用同步方法
五、Lambda 表达式
以上示例是使用AddressOf来关联委托的方法,为了使代码更具可读性,可以使用Lambda 表达式,代码如下:
Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
'暂时移除Button1按钮点击事件
RemoveHandler Button1.Click, AddressOf Button1_Click
'******************************************
Dim method As MethodInvoker = Sub()
'模拟长时间工作代码
For i As Integer = 0 To 100
'模拟长时间工作
Threading.Thread.Sleep(100)
'显示工作进度
Dim Actiondelegate As Action(Of Integer) = Sub(int)
Me.Label1.Text = int.ToString & "%"
End Sub
Me.Label1.Invoke(Actiondelegate, i)
Next
End Sub
method.BeginInvoke(Nothing, Nothing)
'******************************************
'工作结束后记得恢复Button1按钮点击事件
AddHandler Button1.Click, AddressOf Button1_Click
End Sub
Lambda 表达式其实就是一个匿名的Sub过程或Function函数,Lambda 表达式可以作为委托类型,当您将 lambda 表达式分配给委托时,可以指定参数名称,但省略其数据类型,以便从委托中获取数据类型。更多的相关知识请阅读官方帮助:
1、Lambda 表达式 (Visual Basic)
2、如何:创建一个 Lambda 表达式 (Visual Basic)
六、.Net Framework中内置委托类型
.Net Framework中提供了一些常用的预定义委托,如Action、Func、Predicate、MethodInvoker、EventHandler等。用到委托的时候建议尽量使用这些委托类型,而不是在代码中定义更多的委托类型。这样既可以减少系统中的类型数目,又可以简化代码。这些委托类型应该可以满足大部分需求。
例如:使用MethodInvoker委托类型时可以省略声明部分:Public Delegate Sub MethodInvoker()
即:
Public Delegate Sub MethodInvoker()
Dim method As MethodInvoker = New MethodInvoker(AddressOf aa)
与
Dim method As MethodInvoker = New MethodInvoker(AddressOf aa)
是一样的,没必要定义委托了。