April 2005 Archives
Several months ago, Roy Osherove posted a discussion of Defensive Event Publishing in .Net that discussed various problems with the "normal" methods of event publishing and raising in .Net. The naive programmer merely calls MyEvent(sender, eventArgs), never suspecting the minefield into which he or she is blithely strolling. Roy's post suggests several progressively more cautious methods of raising events to protect oneself against "bad" clients. At the time I commented that further improvements could be made, specifically to both avoid using Threadpool threads and to detect which callers are bad. I thought I'd finally get around to explaining what I meant and actually providing a solution I've used in the past.
First off, not using Threadpool threads. I'm really not a fan of using the Threadpool for any operation that I don't have absolute control over, because there's a limited number of them. The default number can be increased, but you can't make it infinite (and if you could, it would defeat the purpose of thread pooling anyway). IMO threadpool threads are useful for short, relatively deterministic operations which won't ever call any client code and which either will never fail, or will fail in such a way that you don't care or can't do anything about anyway. Raising events just doesn't fit those qualifications for me. So the solution is to not use threadpool threads; this is a fairly simple thing to do if you're at all familiar with .Net threading. Depending on your implementation, however, and definitely if you use the code I've posted at the end of this article, then there are a few caveats to watch for; I'll note them along the way.
The second way in which we can add to Roy's article is in detecting failed calls. His solution calls a OneWay async Invoke on the delegate; it's a fire-and-forget situation. Unfortunately, especially for an application that needs to stay up 24/7 for long periods of time, it may not be acceptable to just ignore failed calls; the app may want to clean up, or at least rid itself of the bad reference and let the GC pick it up. In order to do that, I use WaitHandles; each thread that I spawn for an individual delegate call will set a WaitHandle when it finishes. (Note that .Net events raised over Remoting automatically time out after a period of time. Using this method with non-remoted events would require additional code to detect timeouts, but would not require any additional code to detect clients that just don't exist anymore.) Here's one of our caveats: WaitHandle.WaitAll can only handle a certain number of handles; on the current .Net implementation (namely .Net 1.0 and 1.1 on Win32) that limit is 64 handles. Calling WaitHandle.WaitAll on > 64 handles will throw an exception. So, should you have more than 64 clients listening to the event, the code will automatically break them up into batches of 64 and wait on each batch sequentially. Another wrinkle is that WaitHandle.WaitAll isn't usable from STA threads--such as those used by Windows Forms--if you're waiting on more than one handle. This can be particularly tricky, as this means you probably can't raise an event using this code on your main Windows Forms UI thread. The code below doesn't handle this case (because our app wasn't a WinForm app and had no STA threads); if your code will be called from STA threads you will need to handle that situation (possibly by raising all events on a new thread).
The final caveat is that only the class that declares an event can modify that event (other than a simple += or -= to add/remove a listener). Thus you can't modify the delegate list to remove a specific listener except from the original class. In order to get around this, my utility function returns a new delegate list that has all of the "bad" clients removed. If your code needs better information about exactly which delegates were removed, you could add either an out param for the "bad" list, or a delegate called for bad clients, etc.
Using the code is fairly simple. The general case looks like this:
1 using System; 2 3 namespace EventTest 4 { 5 public delegate void MyEventHandler(object sender, EventArgs e); 6 7 public class EventRaiser 8 { 9 public event MyEventHandler MyEvent; 10 11 public void RaiseEvent() 12 { 13 MyEvent = (MyEventHandler)EventRemoter.RaiseRemotedEvent(MyEvent, this, EventArgs.Empty); 14 } 15 } 16 }
Relatively simple, aside from the need to cast the return value and the WaitHandle issues mentioned above.
The code for EventRemoter is available here. If you find it useful, or find a problem or just have a comment, please, let me know!
This code is covered by the same license as other items available from this blog, namely the Creative Commons' "By Attribution 2.0" license.
Addendum: After a brief conversation with someone who had recently asked me about this code, I added a static parameter to control the number of simultaneous threads that will be used by any one event raise, rather than using a magic number sprinkled through the code. The parameter defaults to 64 in order to be correct on Win32, but can be changed in either of two situations. If you want the code to use fewer threads (as the default version will spawn a lot of (very short-lived) threads when raising events to a lot of subscribers), then set the parameter lower. If you are using the code on a platform where WaitAll works with more than 64 handles, then you can set the parameter higher. The new version is at the same location linked above; enjoy!