Async Waiting for deadlock
I am working with a sensor Accelerometer
on Windows Phone 8.1. I have to access the UI from the ReadingChanged
sensor callback . I also have DispatcherTimer
one that updates the ReportInterval
sensor every two seconds . The program blocks when the timer fires and tries to set the ReportInterval of the accelerometer. The example below is a minimal 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 is happening. Thank you in advance.
source to share
Well, I'm stumped.
Dispatcher.RunAsync
should not be deadlocked. So in order to figure out exactly where the problem is, I rewrote your code to a few 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 var window = view.CoreWindow;
. It's hard to explain why without seeing the WinRT source code, I'm guessing that there is some strange interaction between WinRT that needs to be switched to the UI thread to get a reference to the window, and the ReportInterval
Accelerometer property that executes the ReadingChanged
event synchronously .
From there I can come up with several solutions:
-
Get the dispatcher in another way:
async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args) { await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { numberAcc++; }); }
Of course, whether this is possible or not depends on your actual code.
-
Rewrite your code to use
Timer
insteadDispatcherTimer
. I understand that you need to use the UI thread to get the value of the textbox (or something like that), but if you are using data binding (with or without the MVVM pattern) then you should be able to read the value of the associated property from any stream -
Change
ReportInterval
in a different thread. Feels like really hacking.void timer_Tick(object sender, object e) { numberTimer++; Task.Run(() => { acc.ReportInterval = acc.ReportInterval++; }); }
source to share
As per @KooKiz explanation and @StephenCleary comment, I found another possible solution. Since we realized that the problem is here:
var window = view.CoreWindow;
we can cache the dispatcher by storing it as an instance variable. In doing so, we avoid accessing it at the same time as the timer:
namespace TryAccelerometer
{
public sealed partial class MainPage : Page
{
private Accelerometer acc;
private DispatcherTimer timer;
private int numberAcc = 0;
private int numberTimer = 0;
private CoreDispatcher dispatcher;
public MainPage()
{
this.InitializeComponent();
this.NavigationCacheMode = NavigationCacheMode.Required;
dispatcher = Windows.ApplicationModel.Core.CoreApplication.MainView.CoreWindow.Dispatcher;
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 dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () =>
{
numberAcc++;
});
}
void timer_Tick(object sender, object e)
{
numberTimer++;
acc.ReportInterval = acc.ReportInterval++;
//acc.ReadingChanged -= acc_ReadingChanged;
}
/// <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.
}
}
}
Thus, there is no deadlock.
source to share
I created this extension after having blocking issues on WinRT and it solved my problems (so far):
using global::Windows.ApplicationModel.Core;
using global::Windows.UI.Core;
public static class UIThread
{
private static readonly CoreDispatcher Dispatcher;
static DispatcherExt()
{
Dispatcher = CoreApplication.MainView.CoreWindow.Dispatcher;
}
public static async Task Run(DispatchedHandler handler)
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, handler);
}
}
Using
public async Task Foo()
{
await UIThread.Run(() => { var test = 0; });
}
source to share