Click here to Skip to main content
15,881,709 members
Articles / Programming Languages / Visual Basic
Article

WaitHandle Exceptions and Work Arounds

Rate me:
Please Sign up or sign in to vote.
4.58/5 (7 votes)
13 Dec 2008CPOL6 min read 45.7K   497   19   3
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 strings and entered about 25 names. I then created an array of WaitHandles. Next, I iterated over the array with a foreach 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.

VB.NET
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.

VB.NET
<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:

VB.NET
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 ForEach 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 foreach 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:

VB.NET
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United States United States
I have been Software developer for the past 10 years. Mostly worked in web world in asp.net and c#. Recently began job using wpf and vb.net.

Also enjoy reading books on politics and American history, listening to music, and COD on XBox 360.

Visit my blog at: www.anothercodesite.com/blog

All posts have source code for both C# and VB.

Comments and Discussions

 
GeneralGreat Job Pin
yuzaihuan6-Mar-14 23:01
yuzaihuan6-Mar-14 23:01 
Thanks for ur explaination, so I can wait for more than 64 handles by this style.
yuzaihuan

GeneralCouple of things Pin
Dmitri Nеstеruk13-Dec-08 12:53
Dmitri Nеstеruk13-Dec-08 12:53 
GeneralRe: Couple of things Pin
johnsontroye13-Dec-08 13:25
johnsontroye13-Dec-08 13:25 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.