RSS 2.0

Personal Info:

Joe Send mail to the author(s) is a lead architect on an OS incubation project at Microsoft, and was the architect for Parallel Extensions to .NET. He is an author and frequent speaker.

Blogroll:
Other
News
 C|Net
 Kuro5hin
 The Register
Technology
 <?xmlhack?>
 Daily WTF
 DevX
 Hacknot
 Java Today
 Microsoft Top 10 Downloads
 MSDN
 MSDN: "Longhorn"
 MSDN: XML Developer Center
 Slashdot
 Techdirt
 theserverside.com
 W3C
 Web Pages That Suck
 XML Cover Pages
 XML Journal
 xml.com
Technology Blogs
 Aaron Skonnard [PluralSight]
 Adam Bosworth [Google]
 Andy Rich [MS/C++]
 Arpan Desai [MS/XML]
 BCL Team [MS]
 Bill Clementson [Lisp]
 Bill de hÓra
 Bruce Eckel [J]
 Bruce Tate [J]
 Casey Chestnut
 Cedric Beust [Google]
 Chris Anderson [MS/Avalon]
 Chris Lyon [MS]
 Christian Weyer
 Clemens Vasters [newtelligence]
 Craig Andera [PluralSight]
 Dan Sugalski [Parrot]
 Daniel Cazzulino
 Dave Chappel
 Dave Roberts [Lisp]
 Dave Thomas [PragProg]
 Dave Winer
 Dion Almaer [J]
 Don Demsak
 Doug Purdy [MS/Indigo]
 Drew Marsh
 Eric Gunnerson [MS]
 Eric Rudder [MS]
 Eric Sink
 Fritz Onion [PluaralSight]
 Gavin King [J/Hibernate]
 Grady Booch [IBM]
 Hervey Wilson [MS/Indigo]
 Hillel Cooperman [MS/Shell]
 Howard Lewis Ship [J/Apache]
 Ingo Rammer [PluralSight]
 James Gosling [J/Sun]
 James Strachan [J/Groovy]
 Jason Matusow [MS/OSS]
 Jeffrey Schlimmer [MS/Indigo]
 Joe Beda [Google]
 Joel Spoelsky
 Jon Udell
 Josh Ledgard [MS/Evang]
 Joshua Allen [MS]
 Lambda
 Larry Osterman [MS]
 Maoni Stephens [MS/CLR]
 Mark Fussell [MS/XML]
 Martin Fowler
 Martin Gudgin [MS/Indigo]
 Me
 Michael Howard [MS]
 Miguel de Icaza [Mono]
 Mike Clark
 Omri Gazitt [MS/Indigo]
 Pat Helland [MS/PAG]
 Pinku Surana
 Raymond Chen [MS]
 Rich Lander [MS/CLR]
 Rob Howard
 Rob Relyea [MS/Avalon]
 Robert Cringely
 S. Somasegar [MS/DevDiv]
 Sam Gentile
 Scoble [MS/Evang]
 Scott Guthrie [MS/WebNet]
 Scott Hanselman
 Sean McGrath [J]
 Simon Fell
 Stanley Lippman [MS/C++]
 Steve Maine
 Steve Swartz [MS/Indigo]
 Steve Vinoski
 Steven Clarke [MS/Usability]
 Stuart Halloway
 Ted Leung
 Ted Neward [DM]
 Tim Bray [Sun]
 Tim Ewald [Mindreef]
 Tim O'Reilly
 Werner Vogels [Amazon]
 Wintellect
 Yasser Shohoud [MS/Indigo]
Top 20
 Brad Abrams [MS/CLR]
 Chris Brumme [MS/CLR]
 Chris Sells [MS/Ultra]
 Cyrus Najmabadi [MS/C#]
 Dominic Cooney [MS/XAF]
 Don Box [MS/Ultra]
 Don Syme [MS/R]
 Guido van Rossum [Python]
 Herb Sutter [MS/C++]
 Ian Griffiths
 Jason Zander [MS/CLR]
 Jim Hugunin [MS/CLR]
 Joel Pobar [MS/CLR]
 Krzysztof Cwalina [MS/CLR]
 Patrick Logan
 Paul Graham
 Rico Mariani [MS/CLR]
 Rory Blyth [MS/DN]
 Sam Ruby
 Wesner Moise
VC/Business Blogs
 Ed Sim
 Fred Wilson
 Jonathan Schwartz [J/Sun]
 Lawrence Lessig [Stanford]
 Mark Cuban
 Michael Hyatt
 Pierre Omidyar
 Ross Mayfield
 VentureBlog
 Weekly Read
Wine, Food & Tea
 The Silk Road of Wine
 Vinography: a wine blog
 Wine Whys

Disclaimer:
The content of this site are my own personal opinions and do not represent my employer's view in anyway.

© 2010, Joe Duffy

 
 Wednesday, February 27, 2008

I’ve mentioned before that the CLR has a central wait routine that is used by any synchronization waits in managed code.  This covers WaitHandles (AutoResetEvent, ManualResetEvent, etc.), CLR Monitors (Enter, Wait), Thread.Join, any APIs that use such things, and the like.  This routine even gets involved for waits that are internal to the CLR VM itself.  This is primarily done so that the runtime can pump appropriately on STAs, and was later used to experiment with fiber-mode scheduler in SQL Server.  Two years ago I showed how to use these capabilities to build a deadlock detection tool via the CLR’s hosting APIs.  Sadly IO-based waits (like FileStream.Read) do not route through this.

The System.Threading.SynchronizationContext class has a very cool (but not widely known) feature that enables you to extend this central wait routine.  To do so requires four steps: subclass SynchronizationContext; call base.SetWaitNotificationRequired; override the virtual Wait method to contain some custom wait logic; and then register your SynchronizationContext via the static SynchronizationContext.SetSynchronizationContext method.  After you do this, most waits that occur on that thread will be redirected through your custom Wait method.

Here's a very simple example of this:

using System;
using System.Threading;

class BlockingNotifySynchronizationContext : SynchronizationContext {
    public BlockingNotifySynchronizationContext() {
        SetWaitNotificationRequired();
    }

    public override int Wait(
            IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) {
        Console.WriteLine("Begin wait: {0} handles for {1} ms",
            waitHandles.Length, millisecondsTimeout);
        int ret = base.Wait(waitHandles, waitAll, millisecondsTimeout);
        Console.WriteLine("Finished wait");
        return ret;
    }
}

class Program {
    public static void Main() {
        SynchronizationContext.SetSynchronizationContext(
            new BlockingNotifySynchronizationContext());
        ManualResetEvent mre = new ManualResetEvent(false);
        mre.WaitOne(1000, false);
    }
}

If you run this, you'll see some messages printed to the console to do with beginning and finishing waits.

A few things are worth noting:

  • The Wait signature looks a lot like WaitForMultipleObjects.  In fact, it's fairly trivial to turn around and call it via a P/Invoke.  Recovering from APCs is a tad tricky however, and you'd have to do all of your own timeout management, message pumping, and the like.
  • You receive an IntPtr[], making it incredibly difficult to correlate the objects being waited on with the actual synchronization objects from which they came (e.g. Monitors, EventWaitHandles, etc.).
  • The code that runs inside Wait is the wait itself.  In other words, when you return, whatever code initiated the wait is going to assume that the API is being honest and truthful.

Another subtlety is that this code, as written, is subject to stack overflow.  Why is that?  In this particular instance, Console.WriteLine may need to block internally because it automatically serializes access to the output stream.  Well, when that blocks, it just goes through the same central wait routine, which calls back out, and so on and so forth.  Obviously this extends to any code that uses locks, including CLR services like cctors.  So the code you write here needs to be very carefully written so as not to ever block recursively.

Notice that some waits do not call out.  The reason is that the callout stems from a routine deep inside the CLR VM itself.  Some waits may occur while a GC is in progress, at which point it’s illegal to invoke managed code.  The CLR just reverts to using its own default wait logic in such cases.

Lastly this is not a foolproof mechanism.  Other components can register their own SynchronizationContexts, replacing the context you’ve installed completely.  This may mean you miss some blocking calls.  If you are building a ThreadPool, you can always reset it each time the thread is returned, or even use your own ExecutionContexts when running them.  It is also possible that such a context will exist by the time you get around to installing your own.  For example, ASP.NET, WinForms, and WPF use custom SynchronizationContexts. 

If such a context exists already when you install this custom one, you can always defer to it for things like CreateCopy, Send, Post, and Wait.  For example, here’s a SynchronizationContext implementation that allows custom before/after wait actions, but otherwise relies on the existing SynchronizationContext (if any) for things like Send, Post, and Wait:

using System;
using System.Threading;

delegate object PreWaitNotification(
    IntPtr[] waitHandles, bool WaitAll, int millisecondsTimeout);
delegate void PostWaitNotification(
    IntPtr[] waitHandles, bool WaitAll, int millisecondsTimeout,
    int ret, Exception ex, object state);

class BlockingNotifySynchronizationContext : SynchronizationContext {
    private SynchronizationContext m_captured;
    private PreWaitNotification m_pre;
    private PostWaitNotification m_post;

    public BlockingNotifySynchronizationContext(
            PreWaitNotification pre, PostWaitNotification post) :
        this(SynchronizationContext.Current, pre, post) {
    }

    public BlockingNotifySynchronizationContext(
            SynchronizationContext captured, PreWaitNotification pre, PostWaitNotification post) {
        SetWaitNotificationRequired();

        m_captured = captured;
        m_pre = pre;
        m_post = post;
    }

    public override SynchronizationContext CreateCopy() {
        return new BlockingNotifySynchronizationContext(
            m_captured == null ? null : m_captured.CreateCopy(), m_pre, m_post);
    }

    public override void Post(SendOrPostCallback cb, object s) {
        if (m_captured != null)
            m_captured.Post(cb, s);
        else
            base.Post(cb, s);
    }

    public override void Send(SendOrPostCallback cb, object s) {
        if (m_captured != null)
            m_captured.Send(cb, s);
        else
            base.Send(cb, s);
    }

    public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) {
        object s = m_pre(waitHandles, waitAll, millisecondsTimeout);
        int ret = 0;
        Exception ex = null;

        try {
            if (m_captured != null)
                ret = m_captured.Wait(waitHandles, waitAll, millisecondsTimeout);
            else
                ret = base.Wait(waitHandles, waitAll, millisecondsTimeout);
        } catch (Exception e) {
            ex = e;
            throw;
        } finally {
            m_post(waitHandles, waitAll, millisecondsTimeout, ret, ex, s);
        }
        return ret;
    }
}

class Program {
    public static void Main() {
        SynchronizationContext.SetSynchronizationContext(
            new BlockingNotifySynchronizationContext(
                delegate { Console.WriteLine("PRE"); return null; },
                delegate { Console.WriteLine("POST"); }
            )
        );
        ManualResetEvent mre = new ManualResetEvent(false);
        mre.WaitOne(1000, false);
    }
}

That’s a fair bit of code, but it's mostly boilerplate.  It allows you to easily specify a pre/post action to be invoked upon each blocking call, and will work on ASP.NET, GUI threads, and the like.  The pre action can return an object for the post action to inspect.  And the post action is given the return value and exception (if any).  If no SynchronizationContext was present when installed, it just defers to the base SynchronizationContext implementation of Send, Post, and Wait.

Now what you actually do inside those callbacks, I suppose, is entirely your business …

2/27/2008 2:47:47 PM (Pacific Standard Time, UTC-08:00)  #    Comments [2]

 

Recent Entries:

Search:

Browse by Date:
<February 2010>
SunMonTueWedThuFriSat
31123456
78910111213
14151617181920
21222324252627
28123456
78910111213

Browse by Category:

Notables:

Currently Up To:

Reading...

Listening...

Watching...