异步等待死锁
Async Await deadlock
我在 Windows Phone 8.1 上使用 Accelerometer
传感器。我必须从传感器的 ReadingChanged
回调中访问 UI。我还有一个 DispatcherTimer
,每两秒更新一次传感器的 ReportInterval
。当计时器触发并尝试设置加速度计的 ReportInterval 时程序会阻塞。下面的示例是重现错误的最小可执行示例。
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.
}
}
}
我不明白为什么会出现死锁。提前谢谢你。
好吧,我被难住了。
Dispatcher.RunAsync
不应该导致死锁。因此,为了找出问题的确切位置,我在多行中重写了您的代码:
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++; });
}
真正的罪魁祸首是var window = view.CoreWindow;
。很难解释为什么没有看到 WinRT 源代码,我猜 WinRT 需要切换到 UI 线程以检索对 window 的引用和 [=15= 之间存在一些奇怪的交互] 属性 Accelerometer 同步执行 ReadingChanged
事件。
从那里,我可以想到一些解决方案:
以另一种方式检索调度程序:
async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args)
{
await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { numberAcc++; });
}
当然,能不能,要看你实际的代码。
重写您的代码以使用 Timer
而不是 DispatcherTimer
。我知道您需要使用 UI 线程来检索文本框(或类似的东西)的值,但是如果您使用数据绑定(有或没有 MVVM 模式),那么您应该能够访问从任何线程
读取绑定属性的值
在另一个线程中更改 ReportInterval
。不过感觉真的很黑。
void timer_Tick(object sender, object e)
{
numberTimer++;
Task.Run(() => { acc.ReportInterval = acc.ReportInterval++; });
}
根据@KooKiz 的解释和@StephenCleary 的评论,我找到了另一种可能的解决方案。由于我们已经了解问题出在这里:
var window = view.CoreWindow;
我们可以缓存调度程序并将其保存为实例变量。这样做,我们避免在计时器的同时访问它:
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.
}
}
}
这样就不会发生死锁。
我在 WinRT 上遇到死锁问题后创建了这个扩展,它解决了我的问题(到目前为止):
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);
}
}
用法
public async Task Foo()
{
await UIThread.Run(() => { var test = 0; });
}
我在 Windows Phone 8.1 上使用 Accelerometer
传感器。我必须从传感器的 ReadingChanged
回调中访问 UI。我还有一个 DispatcherTimer
,每两秒更新一次传感器的 ReportInterval
。当计时器触发并尝试设置加速度计的 ReportInterval 时程序会阻塞。下面的示例是重现错误的最小可执行示例。
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.
}
}
}
我不明白为什么会出现死锁。提前谢谢你。
好吧,我被难住了。
Dispatcher.RunAsync
不应该导致死锁。因此,为了找出问题的确切位置,我在多行中重写了您的代码:
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++; });
}
真正的罪魁祸首是var window = view.CoreWindow;
。很难解释为什么没有看到 WinRT 源代码,我猜 WinRT 需要切换到 UI 线程以检索对 window 的引用和 [=15= 之间存在一些奇怪的交互] 属性 Accelerometer 同步执行 ReadingChanged
事件。
从那里,我可以想到一些解决方案:
以另一种方式检索调度程序:
async void acc_ReadingChanged(Accelerometer sender, AccelerometerReadingChangedEventArgs args) { await this.Dispatcher.RunAsync(Windows.UI.Core.CoreDispatcherPriority.Normal, () => { numberAcc++; }); }
当然,能不能,要看你实际的代码。
重写您的代码以使用
Timer
而不是DispatcherTimer
。我知道您需要使用 UI 线程来检索文本框(或类似的东西)的值,但是如果您使用数据绑定(有或没有 MVVM 模式),那么您应该能够访问从任何线程 读取绑定属性的值
在另一个线程中更改
ReportInterval
。不过感觉真的很黑。void timer_Tick(object sender, object e) { numberTimer++; Task.Run(() => { acc.ReportInterval = acc.ReportInterval++; }); }
根据@KooKiz 的解释和@StephenCleary 的评论,我找到了另一种可能的解决方案。由于我们已经了解问题出在这里:
var window = view.CoreWindow;
我们可以缓存调度程序并将其保存为实例变量。这样做,我们避免在计时器的同时访问它:
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.
}
}
}
这样就不会发生死锁。
我在 WinRT 上遇到死锁问题后创建了这个扩展,它解决了我的问题(到目前为止):
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);
}
}
用法
public async Task Foo()
{
await UIThread.Run(() => { var test = 0; });
}