superpuccio superpuccio - 3 months ago 40
C# Question

Async Await deadlock

I'm working with the

Accelerometer
sensor on Windows Phone 8.1. I must access the UI from the
ReadingChanged
callback of the sensor. I also have a
DispatcherTimer
that, every two seconds, updates the
ReportInterval
of the sensor. The program blocks when the timer fires and try to set the ReportInterval of the Accelerometer. The example here below is a minimum executable example that reproduces the error.

namespace TryAccelerometer
{
public sealed partial class MainPage : Page
{
private Accelerometer acc;
private DispatcherTimer timer;
private int numberAcc = 0;
private int numberTimer = 0;

public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;

acc = Accelerometer.GetDefault();
acc.ReadingChanged += acc_ReadingChanged;

timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(2);
timer.Tick += timer_Tick;
timer.Start();
}

async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args)
{
await Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
//HERE I WILL HAVE TO ACCESS THE UI, BUT FOR SAKE OF SIMPLICITY I WROTE AN INCREMENT
numberAcc++;
});
}

void timer_Tick(object sender, object e)
{
numberTimer++;
//PUT A BREAKPOINT HERE BELOW AND SEE THAT THE PROGRAM BLOCKS
acc.ReportInterval = acc.ReportInterval++;
}
/// <summary>
/// Invoked when this page is about to be displayed in a Frame.
/// </summary>
/// <param name="e">Event data that describes how this page was reached.
/// This parameter is typically used to configure the page.</param>
protected override void OnNavigatedTo(NavigationEventArgs e)
{
// TODO: Prepare page for display here.

// TODO: If your application contains multiple pages, ensure that you are
// handling the hardware Back button by registering for the
// Windows.Phone.UI.Input.HardwareButtons.BackPressed event.
// If you are using the NavigationHelper provided by some templates,
// this event is handled for you.
}
}
}


I don't understand why the deadlock happens. Thank you in advance.

Answer

Well I'm stumped.

Dispatcher.RunAsync shouldn't possibly cause a deadlock. Therefore, to find out where exactly the issue is, I rewrote your code on multiple lines:

async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args)
{
    var view = Windows.ApplicationModel.Core.CoreApplication.MainView;

    var window = view.CoreWindow;

    var dispatcher = window.Dispatcher;

    await dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { numberAcc++; });
}

The real culprit is var window = view.CoreWindow;. It's hard to explain why without seeing the WinRT source code, I guess there is some weird interaction between WinRT needing to switch to the UI thread to retrieve the reference to the window, and the ReportInterval property of the Accelerometer executing synchronously the ReadingChanged event.

From there, I can think of a few solutions:

  1. Retrieve the dispatcher another way:

    async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args)
    {
        await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { numberAcc++; });
    }
    

    Of course, whether it's possible or not depends on your actual code.

  2. Rewrite your code to use a Timer instead of a DispatcherTimer. I understand that you need to use the UI thread to retrieve the value of a textbox (or something like that), but if you use databinding (with or without the MVVM pattern), then you should be able to access the read the value of the bound property from any thread

  3. Change the ReportInterval in another thread. Feels really hackish though.

    void timer_Tick(object sender, object e)
    {
        numberTimer++;
        Task.Run(() => { acc.ReportInterval = acc.ReportInterval++; });
    }