Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Replacement for SynchronizationContext.Send() in Portable Class Libraries

I'm writing a Portable Class Library that is going to be used by WPF, Windows Phone and possibly WinRT apps and I'm doing some work on background threads that occasionally need to call back to the UI. I instantiate the classes doing this in the UI thread, so I can easily save the SynchronizationContext and use it to call back to the UI.

However, in PCL, SynchronizationContext.Send() is obsolete, because it's not supported by WinRT and SynchronizationContext.Post() (which runs asynchronously) is not always appropriate.

I figured I'd just wait until the delegate passed to Post() is run, but all my attempts ended with a deadlock if Post() was invoked from the same thread the saved SynchronizationContext referred to.

Now I've managed to fix this by checking if it's the same thread and just simply calling my delegate if it is, but the checks are incredibly ugly involving reflecting out the value of private fields of the API, so I thought someone could help me find a more proper way.

Here is my current code if you'd like to see some gore:

/// <summary>
/// Invokes the passed callback on this SynchronizationContext and waits for its execution. Can be used even if
/// SynchronizationContext.Send is not available. Throws the exceptions thrown in the delegate.
/// </summary>
/// <param name="context">the context to run the method</param>
/// <param name="d">the method to run</param>
/// <param name="state">the parameter of the method to run</param>
public static void InvokeSynchronized( this SynchronizationContext context, SendOrPostCallback d, object state )
{
    if ( !context.Match( SynchronizationContext.Current ) )
    {
        ManualResetEvent waitHandle = new ManualResetEvent( false );
        Exception error = null;

        // replicate SynchronizationContext.Send with .Post as Send is obsolete in the Portable Class Library
        context.Post( ( o ) =>
            {
                try
                {
                    d( o );
                }
                catch ( Exception exc )
                {
                    error = exc;
                }
                finally
                {
                    waitHandle.Set();
                }
            },
        state );

        waitHandle.WaitOne();

        if ( error != null )
        {
            throw error;
        }
    }
    else
    {
        d( state );
    }
}

/// <summary>
/// Checks if the two SynchronizationContexts refer to the same thread
/// </summary>
/// <param name="sc1"></param>
/// <param name="sc2"></param>
/// <returns></returns>
public static bool Match(this SynchronizationContext sc1, SynchronizationContext sc2)
{
    if ( sc2 == null )
    {
        return false;
    }
    else if ( sc1 == sc2 || sc1.Equals(sc2) )
    {
        return true;
    }

    // check if the two contexts run on the same thread
    // proper equality comparison is generally not supported, so some hacking is required

    return sc1.FindManagedThreadId() == sc2.FindManagedThreadId();
}

/// <summary>
/// Finds the ManagedThreadId of the thread associated with the passed SynchronizationContext
/// </summary>
/// <param name="sc"></param>
/// <returns></returns>
public static int FindManagedThreadId(this SynchronizationContext sc)
{
    // here be dragons
    try
    {
        BindingFlags bindFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static;

        switch ( sc.GetType().FullName )
        {
            case "System.Windows.Threading.DispatcherSynchronizationContext":
                // sc._dispatcher.Thread.ManagedThreadId
                var _dispatcher_field = sc.GetType().GetField( "_dispatcher", bindFlags );
                var _dispatcher_value = _dispatcher_field.GetValue( sc );

                var Thread_property = _dispatcher_value.GetType().GetProperty( "Thread", bindFlags );
                var Thread_value = Thread_property.GetValue( _dispatcher_value, null ) as Thread;

                return Thread_value.ManagedThreadId;
        }
    }
    catch ( Exception e )
    {
        throw new InvalidOperationException( "ManagedThreadId could not be obtained for SynchronizationContext of type " + sc.GetType().FullName, e );
    }

    throw new InvalidOperationException( "ManagedThreadId not found for SynchronizationContext of type " + sc.GetType().FullName );

}

Thanks!

like image 322
KáGé Avatar asked Mar 16 '14 19:03

KáGé


1 Answers

I think that SynchronizationContext.Send is being deprecated by Microsoft for a good reason. They really want the new Windows Store and WP8 apps to fully embrace asynchronous programming model and make the blocking code a thing of the past.

So, something that was:

void SendDataToUI()
{
    _synchronizationContext.Send(_callback, data);
}

Should now become:

async Task SendDataToUIAsync()
{
    var tcs = new TaskCompletionSource<object>();
    _synchronizationContext.Post(a => 
    { 
        try
        {
            _callback(a);
            tcs.SetResult(Type.Missing);
        }
        catch (Exception ex)
        {
            tcs.SetException(ex);
        }
    }, data);

    await tcs.Task;
}

That said, I suppose you have your own good reasons to use SynchronizationContext.Send in your PCL library.

The first part of your logic looks good, and you could cut off the reflection part of it by simply memorizing the Thread.CurrentThread.ManagedThreadId of the UI thread, at the same place where you memorize the SynchronizationContext.Current of the UI thread. Then in your implementation of InvokeSynchronized you just compare it to the Thread.CurrentThread.ManagedThreadId of the current thread, and use waitHandle.WaitOne() if your are on non-UI thread.

like image 151
noseratio Avatar answered Oct 31 '22 16:10

noseratio