在之前的一篇文章中,讨论了如何使用异步调用来保持用户界面的响应性。这次,遇到了一个稍微复杂一点的问题:在VB.NET中,如何异步调用同一个方法多次,并阻塞直到所有线程完成。这个问题在VB.NET中并不简单,让一步步来探讨。
为了测试方法,创建了一个简单的VB.NET控制台应用程序。在这个程序中,创建了一个包含大约25个名字的字符串数组,然后创建了一个WaitHandle数组。接下来,使用for…each循环遍历数组,同时调用委托BeginInvoke,并把返回的IAsyncResult添加到WaitHandle数组中。然后,立即调用WaitHandle.WaitAll来阻塞当前线程,直到所有线程完成。委托指向一个简单的子程序,它将名字写入控制台窗口。这个子程序还会检查名字是否为"Ryan",如果是,则暂停线程。这样做是为了在控制台窗口中看到异步调用的效果。
以下是VB.NET中的代码示例:
Private Sub WriteName(ByVal p_Name As String)
If p_Name = "Ryan" Then
Thread.Sleep(2000)
End If
Console.WriteLine(p_Name)
End Sub
Private Sub UsingWaitAll()
Dim names As String() = GetNames(200)
Dim waitHandles As List(Of WaitHandle) = New List(Of WaitHandle)()
Dim caller As WriteNameHandler = New WriteNameHandler(AddressOf WriteName)
For Each name As String In names
Dim results As IAsyncResult = caller.BeginInvoke(name, Nothing, Nothing)
waitHandles.Add(results.AsyncWaitHandle)
Next
WaitHandle.WaitAll(waitHandles.ToArray())
Console.WriteLine("Done")
End Sub
按照预期,所有新线程都应该异步启动,WaitAll调用将阻塞。一旦所有线程完成任务,它们应该发出信号,然后WaitHandle继续进一步处理。但事实并非如此,遇到了一个异常:“WaitAll在STA线程上不支持多个句柄。”
在研究这个问题时,发现了几种解决方法。由于对这个问题的本质理解不够深入,很难说哪种解决方案是可行的。以下是找到的三种解决方案:
第一种解决方案是将Sub Main()的属性更改为MTAThreadAttribute()。从STA(单线程单元)更改为MTA(多线程单元)后,应用程序可以正常完成,然后结束,没有问题。
<MTAThreadAttribute()> _
Sub Main()
' some code here
End Sub
第二种解决方案是使用自定义的WaitAll方法。这个方法检查当前线程是否为STA线程,如果是,则循环遍历WaitHandle数组,调用WaitAny方法。
Private Sub UsingWaitAllTrick()
Dim names As String() = GetNames(100)
Dim waitHandles As List(Of WaitHandle) = New List(Of WaitHandle)()
Dim caller As WriteNameHandler = New WriteNameHandler(AddressOf WriteName)
For Each name As String In names
Dim results As IAsyncResult = caller.BeginInvoke(name, Nothing, Nothing)
waitHandles.Add(results.AsyncWaitHandle)
Next
WaitAll(waitHandles.ToArray())
End Sub
Sub WaitAll(ByVal waitHandles() As WaitHandle)
If Thread.CurrentThread.GetApartmentState() = ApartmentState.STA Then
For Each waitHandle As WaitHandle In waitHandles
waitHandle.WaitAny(New WaitHandle() {waitHandle})
Next
Else
WaitHandle.WaitAll(waitHandles)
End If
End Sub
Dim manualResetEvent As ManualResetEvent
Dim remainingWorkItems As Integer = 0
Dim objLock1 As Object = New Object
Private Sub UsingWaitAllManualResetEvent()
manualResetEvent = New ManualResetEvent(False)
Dim names As String() = GetNames(100)
For Each name As String In names
SyncLock objLock1
remainingWorkItems += 1
End SyncLock
ThreadPool.QueueUserWorkItem(AddressOf WriteNameManualResetEvent, name)
Next
manualResetEvent.WaitOne()
Console.WriteLine("Done")
End Sub