异步等待死锁

3

我正在使用Windows Phone 8.1上的加速度计传感器。我必须从传感器的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.
        }
    }
}

我不明白死锁为什么会发生。谢谢您提前。


1
我在 WP 上没有看到过这个问题,但是一些 MS UI 框架在初始化时存在问题。在调用 Accelerometer.GetDefault 之前尝试访问 CoreWindow.Dispatcher。一些 XAML 框架会按需创建分派程序,如果 ReadingChanged 在分派程序循环开始之前触发,则可能会导致奇怪的问题。 - Stephen Cleary
3个回答

3

我被难住了。

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线程以检索对窗口的引用,并且Accelerometer的ReportInterval属性同步执行ReadingChanged事件导致了一些奇怪的交互。
从这里开始,我可以想到几个解决方案:
  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++; });
    }
    

非常感谢。我不能应用你的第一个解决方案,因为我的实际“this”不是控件,也许第三个解决方案有点棘手,但第二个解决方案很完美。你是如何调试它的?我知道你把代码分成了几行,但接下来呢?你能解释一下你是如何理解问题确切出现在这一行的吗 --> "var window = view.CoreWindow;" - superpuccio
1
@superpuccio 在你重现死锁后,点击Visual Studio的“暂停”图标以进入调试器,然后使用任务视图(调试->窗口->并行堆栈)查看每个线程的调用堆栈。然后双击你想要的堆栈帧,你就可以看到相应的代码了。 - Kevin Gosse
我在下面添加了另一个可能的解决方案。我想听听你的反馈,也许你可以试试。谢谢。 - superpuccio
1
@superpuccio 我们通常使用同步上下文来处理这个问题,但是,是的,它是一个完全有效的解决方案。 - Kevin Gosse

1

在 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; });
}

1

根据@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.
        }
    }
}

这样就不会发生死锁。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接