WaitHandle Exceptions and Work Arounds






4.58/5 (7 votes)
Provide solutions to some common exceptions that can be thrown in asynchronous applications when trying to block and signal using WaitHandle.WaitAll.
Introduction
This article talks about solutions to two different exceptions that can get thrown when using WaitHandle.WaitAll
in a VB.NET Console Application. Exception #1 deals with an exception thrown when the application is running on an STA which is the default for VB Console App. However, C# Console Apps run under MTA by default and as a result do not get this error. For that reason, the C# download only contains Solution #3 which helps work around Exception #2 dealing with the max number of WaitHandles
allowed. Finally, some of the asynchronous method calls are slightly different in C# than in VB. I am only writing the VB code in this post. If you would like to view the C# code, you will need to download that solution. I am only writing the VB code in this post. If you would like to view the C# code, you will need to download that solution.
Background
In a previous post, I covered how to keep a UI responsive using asynchronous calls. The focus on that article was using asynchronous calls on long running processes in an effort to keep the main UI thread responsive. Recently, I wanted to do something similar, with a slight twist. I needed to call the same method multiple times asynchronously while blocking until all threads were completed. My first stab was in VB.NET and what I thought was going to be trivial actually turned out to be another trip down the rabbit hole. Let me explain.
Using the Code
To test my approach, I created a simple VB.NET console application. In it, I created an array of string
s and entered about 25 names. I then created an array of WaitHandles
. Next, I iterated over the array with a for
…each
loop while calling BeginInvoke
on the delegate
I created and also adding the IAsyncResult
which gets returned from BeginInvoke
to an array of WaitHandles
. I then immediately blocked further processing on that thread by calling WaitHandle.WaitAll
and passing in the array of WaitHandles
. The delegate
pointed to a simple subroutine which wrote the name to the console window. This sub also checked if name is “Ryan” and if so paused the thread. I did this so I could see in the console window that the calls were being made asynchronously.
Delegate Sub WriteNameHandler(ByVal p_Name As String)
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)
Next
WaitHandle.WaitAll(waitHandles.ToArray())
Console.WriteLine("Done")
End Sub
Private Sub WriteName(ByVal name As String)
WriteName(ByVal name As String)
If name = "Ryan" Then
Thread.Sleep(2000)
End If
Console.WriteLine(name)
End Sub
What I expected, and what the documentation in MSDN said should have happened was the new threads all get spawned off asynchronously and the WaitAll
call will block. Once all the threads completed their task, they should signal the WaitHandle
which should then proceed with further processing.
Not to be. Here is the exception thrown when I tried this approach:
“WaitAll for multiple handles on a STA thread is not supported.”
Off I was to find out what this meant, and why I was getting it. After much research, I can't say that I still have a solid understanding of what this is all about. However, my high level understanding is that VB console applications run in a STA (Single Threaded Apartment). An STA application does not like multiple handles. Since we are creating multiple handles, it chokes.
There are several ways I found to circumvent this issue. However, since I don't have a great understanding of what the real issue is; it’s hard for me to say which of these solutions are viable and which are not. Hopefully some subject matter experts will ring in on this topic to explain.
Solution 1
The first and easiest solution I found was to decorate Sub Main()
with the following attribute and line continuation character: <MTAThreadAttribute()> _
. Changing from STA to MTA allowed the application to finish, then end with no problem. One note here, I also worked this up for a WPF application. The WPF application will not compile if you try to change the SubMain
from <System.STAThreadAttribute()> _
to <System.MTAThread()> _
. You get a compilation error saying there are too many UI components that rely on STA to make this change.
<MTAThreadAttribute()> _
Sub Main()
' some code here
End Sub
In addition, this does not solve the problem of the second exception I was getting which was:
“The number of WaitHandles must be less than or equal to 64.”
This would happen as you probably guessed, if I tried to add more than 64 items into the waithandle
array and called WaitHandle.WaitAll
. This is the result of the WaitAll
method call however, not just the number waithandles
. I know this because in solution 2, I am still adding more than 64 waithandles
to the array, but I am no longer calling WaitHandle.WaitAll
and everything runs without exceptions. In addition, if I remove the WaitHandle.WaitAll
method in Solution 1, things also run without exceptions. However, you obviously have removed the blocking call at the same time.
Solution 2
During my research, I found another way around this problem that several bloggers had posted. However, it’s one of those solutions that I looked at and said, “Yes, it works, but it just doesn't seem like a good idea.” Admittedly, I don't have any solid reason for why I don’t like it, other than a gut feeling. So I will post it here again hoping that someone will comment on it. This method replaces the CLR method WaitHandle.WaitAll
with the following method:
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
I then replaced my call: codewaithandle.waitall(_waithandles.toarray()) in Sub Main
with: WaitAll(_waitHandles.ToArray())
. This sub routine now acts as the thread blocker. It checks to see if the thread is an STA thread, which it always will be for a VB console application. It then loops over these wait handles calling waithandle
. WaitAny
waits for any of the elements in the array of WaitHandles
to receive a signal. Once they all have signaled, the For
…Each
terminates, and processing continues on the main thread. As a note, the threads send a signal automatically once they have completed. You do not have to do this manually. This method obviously also worked for the WPF application. Remember, this method also solved the issue with having to have less than 64 waithandles
.
Solution 3
In an effort to build a better mouse trap, I kept looking for another way to solve this problem. What I found was the ManualResetEvent.WaitOne
method. The ManualResentEvent
derives from EventWaitHandle
and uses similar blocking and signaling methods. The ManualResetEvent.WaitOne
method is used to block one thread while other threads complete. What I did was create a static
variable which gets incremented for each new thread that is created inside a for
…each
loop. The next line of code is the blocking call manualResetEvent.WaitOne
. This will block until it is signaled. How does it get signaled? The method being called asynchronously decrements the static
counter which is tracking the number of threads. It then checks to see if that count is zero, meaning all the threads have completed. When the counter is zero, manualResetEvent.Set
is called which signals the manualResetEvent
that it may stop blocking and allow process to continue on the main thread:
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
' wait for reset to be signaled. This will happen when
' remainingWorkItems() equals 0 in callback method.
manualResetEvent.WaitOne()
Console.WriteLine("Done")
End Sub
Notice that a lock is needed when reading or writing to the counter variable remainingWorkItems
. I hope this can save someone some time in trying to get around these areas. Any comments or critiques are welcome, especially if I'm suggesting something that is not a best practice. The solution I used to demonstrate the three solutions in this post will be posted in both C# and VB.NET. Just remember though, the STA issue only occurs if it is a VB console or WPF application.
History
- 13th December, 2008: Initial post