如何让UI线程等待信号量,但处理额外的调度程序请求?(就像MessageBox.Show本身所做的一样)

本文关键字:Show MessageBox 就像 一样 调度程序 线程 等待 UI 信号量 处理 请求 | 更新日期: 2023-09-27 18:29:21

通常,当UI线程调用类似MessageBox.Show()的东西时,当前代码执行不会继续,直到用户单击"确定",但程序将继续运行在UI线程上调度的其他代码。

在这个问题中,我遇到了一个问题,即在UI线程上同时调用了太多委派。在继续执行之前,我想在某些时刻停下来。

在我的新错误处理程序中,我使用信号量来确保一次处理的错误不超过一个。我发送一个MessageBox来提醒用户,当他们单击"确定"时,我释放信号量,允许处理下一个错误。

问题是它的行为没有达到预期。如果同时发生对HandleError的两个调度调用,则第一个调度对MessageBox.Show的调用,第二个阻塞UI线程。奇怪的是,对MessageBox.Show()的调度调用从未被执行——整个应用程序只是挂起——因此,当用户单击"OK"时应该释放的信号量被永久锁定。这个解决方案缺少什么?

private static ConcurrentDictionary<Exception, DateTime> QueuedErrors = new ConcurrentDictionary<Exception, DateTime>();
private static Semaphore Lock_HandleError = new Semaphore(1, 1); //Only one Error can be processed at a time
private static void ErrorHandled(Exception ex)
{
    DateTime value;
    QueuedErrors.TryRemove(ex, out value);
    Lock_HandleError.Release();
}
private static bool ExceptionHandlingTerminated = false;
public static void HandleError(Exception ex, string extraInfo = "", bool showMsgBox = true, bool resetApplication = true)
{
    if( ExceptionHandlingTerminated || App.Current == null) return;
    QueuedErrors.TryAdd(ex, DateTime.Now); //Thread safe tracking of how many simultaneous errors are being thrown
    Lock_HandleError.WaitOne(); //This will ensure only one error is processed at a time.
    if( ExceptionHandlingTerminated || App.Current == null )
    {
        ErrorHandled(ex);
        return;
    }
    try
    {
        if( QueuedErrors.Count > 10 )
        {
            ExceptionHandlingTerminated = true;
            throw new Exception("Too many simultaneous errors have been thrown in the background.");
        }
        if( Thread.CurrentThread != Dispatcher.CurrentDispatcher.Thread )
        {
            //We're not on the UI thread, we must dispatch this call.
            ((App)App.Current).Dispatcher.BeginInvoke((Action<Exception, string, bool, bool>)
                delegate(Exception _ex, string _extraInfo, bool _showMsgBox, bool _resetApplication)
                {
                    ErrorHandled(_ex); //Release the semaphore taken by the spawning HandleError call
                    HandleError(_ex, _extraInfo, _showMsgBox, _resetApplication);
                }, DispatcherPriority.Background, new object[] { ex, extraInfo, showMsgBox, resetApplication });
            return;
        }
        if( showMsgBox )
        {
            //IF the UI is processing a visual tree event (such as IsVisibleChanged), it throws an exception when showing a MessageBox as described here: http://social.msdn.microsoft.com/forums/en-US/wpf/thread/44962927-006e-4629-9aa3-100357861442
            //The solution is to dispatch and queue the MessageBox. We must use BeginInvoke because dispatcher processing is suspended in such cases.
            Dispatcher.CurrentDispatcher.BeginInvoke((Action<Exception, String>)delegate(Exception _ex, String _ErrMessage)
            {
                MessageBox.Show(_ErrMessage, "MUS Application Error", MessageBoxButton.OK, MessageBoxImage.Error);
                ErrorHandled(_ex); //Release the semaphore taken by the spawning HandleError call
            }, DispatcherPriority.Background, new object[]{ ex, extraInfo });
        }
        else
        {
            ErrorHandled(ex);
        }
    }
    catch( Exception terminatingError )
    {
        ExceptionHandlingTerminated = true;
        Dispatcher.CurrentDispatcher.BeginInvoke((Action<String>)delegate(String _fatalMessage)
        {
            MessageBox.Show(_fatalMessage, "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Stop);
            if( App.Current != null ) App.Current.Shutdown(1);
        }, DispatcherPriority.Background, new object[] { fatalMessage });
        ErrorHandled(ex); //Release the semaphore taken by this HandleError call which will allow all other queued HandleError calls to continue and check the ExceptionHandlingTerminated flag.
    }
}

不要担心奇怪的消息字符串丢失,我删掉了很多细节以使模式更清晰。

如何让UI线程等待信号量,但处理额外的调度程序请求?(就像MessageBox.Show本身所做的一样)

假设您要查找的行为是每个消息框依次等待,直到上一个消息框被清除,那么您需要这样的模式:

  1. 事件源将消息排入阻塞队列
  2. 事件源调用后台线程上的委托以"处理队列"
  3. "处理队列"委托获取一个锁(正如您所做的那样),将消息出列,并(同步)调用到UI线程以显示消息。然后它循环,做同样的事情,直到队列emtpy

所以类似这样的东西(未经测试的代码):

private static ConcurrentQueue<Tuple<Exception, DateTime>> QueuedErrors = new ConcurrentQueue<Tuple<Exception, DateTime>>();
private static Object Lock_HandleError = new Object();
public static void HandleError(Exception ex, string extraInfo = "", bool showMsgBox = true, bool resetApplication = true)
{
    QueuedErrors.Enqueue(new Tuple<Exception, String>(ex, DateTime.Now));
    ThreadPool.QueueUserWorkItem(()=>((App)App.Current).Dispatcher.Invoke((Action)
            () => {
                lock (Lock_HandleError)
                    Tuple<Exception, DateTime> currentEx;
                    while (QueuedErrors.TryDequeue(out currentEx))
                        MessageBox.Show(
                           currentEx.Item1, // The exception
                           "MUS Application Error", 
                           MessageBoxButton.OK, 
                           MessageBoxImage.Error);
            }))
    );

我决定按照建议将它们存储在集合中。我只是按顺序处理错误,然后从堆栈中弹出一个新的错误(如果有的话)。如果堆栈上累积了太多错误,那么我假设我们处于级联错误的情况下,我将错误聚合在一条消息中,然后关闭应用程序。

private static ConcurrentStack<Tuple<DateTime, Exception, String, bool, bool>> ErrorStack = new ConcurrentStack<Tuple<DateTime, Exception, String, bool, bool>>();
private static bool ExceptionHandlingTerminated = false;
private static bool ErrorBeingHandled = false; //Only one Error can be processed at a time
public static void HandleError(Exception ex, bool showMsgBox) { HandleError(ex, "", showMsgBox, true); }
public static void HandleError(Exception ex, string extraInfo, bool showMsgBox) { HandleError(ex, extraInfo, showMsgBox, true); }
public static void HandleError(Exception ex, string extraInfo = "", bool showMsgBox = true, bool resetApplication = true)
{
    if( ExceptionHandlingTerminated || App.Current == null) return;
    if( ErrorBeingHandled )
    {   //Queue up this error, it'll be handled later. Don't bother if we've already queued up more than 10 errors, we're just going to be terminating the application in that case anyway.
        if( ErrorStack.Count < 10 )
            ErrorStack.Push(new Tuple<DateTime, Exception, String, bool, bool>(DateTime.Now, ex, extraInfo, showMsgBox, resetApplication)); //Thread safe tracking of how many simultaneous errors are being thrown
        return;
    }
    ErrorBeingHandled = true;
    try
    {
        if( Thread.CurrentThread != Dispatcher.CurrentDispatcher.Thread )
        {
            ErrorBeingHandled = false;
            Invoke_HandleError( ex, extraInfo, showMsgBox, resetApplication );
            return;
        }
        if( ErrorStack.Count >= 5 )
        {
            ExceptionHandlingTerminated = true;
            Tuple<DateTime, Exception, String, bool, bool> errParams;
            String errQueue = String.Concat(DateTime.Now.ToString("hh:mm:ss.ff tt"), ": ", ex.Message, "'n");
            while( ErrorStack.Count > 0 )
            {
                if( ErrorStack.TryPop(out errParams) )
                {
                    errQueue += String.Concat(errParams.Item1.ToString("hh:mm:ss.ff tt"), ": ", errParams.Item2.Message, "'n");
                }
            }
            extraInfo = "Too many simultaneous errors have been thrown in the background:";
            throw new Exception(errQueue);
        }
        if( !((App)App.Current).AppStartupComplete )
        {   //We can't handle errors the normal way if the app hasn't started yet.
            extraInfo = "An error occurred before the application could start." + extraInfo;
            throw ex;
        }
        if( resetApplication )
        {
            ((MUSUI.App)App.Current).ResetApplication();
        }
        if( showMsgBox )
        {
            //(removed)... Prepare Error message
            //IF the UI is processing a visual tree event (such as IsVisibleChanged), it throws an exception when showing a MessageBox as described here: http://social.msdn.microsoft.com/forums/en-US/wpf/thread/44962927-006e-4629-9aa3-100357861442
            //The solution is to dispatch and queue the MessageBox. We must use BeginInvoke because dispatcher processing is suspended in such cases.
            Dispatcher.CurrentDispatcher.BeginInvoke((Action<Exception, String>)delegate(Exception _ex, String _ErrMessage)
            {
                MessageBox.Show(App.Current.MainWindow, _ErrMessage, "MUS Application Error", MessageBoxButton.OK, MessageBoxImage.Error);
                ErrorHandled(_ex); //Release the block on the HandleError method and handle any additional queued errors.
            }, DispatcherPriority.Background, new object[]{ ex, ErrMessage });
        }
        else
        {
            ErrorHandled(ex);
        }
    }
    catch( Exception terminatingError )
    {
        ExceptionHandlingTerminated = true;
        //A very serious error has occurred, such as the application not loading, and we must shut down.
        Dispatcher.CurrentDispatcher.BeginInvoke((Action<String>)delegate(String _fatalMessage)
        {
            MessageBox.Show(_fatalMessage, "Fatal Error", MessageBoxButton.OK, MessageBoxImage.Stop);
            if( App.Current != null ) App.Current.Shutdown(1);
        }, DispatcherPriority.Background, new object[] { fatalMessage + "'n" + terminatingError.Message });
    }
}
//The set of actions to be performed when error handling is done.
private static void ErrorHandled(Exception ex)
{
    ErrorBeingHandled = false;
    //If other errors have gotten queued up since this one was being handled, or remain, process the next one
    if(ErrorStack.Count > 0)
    {
        if( ExceptionHandlingTerminated || App.Current == null) return;
        Tuple<DateTime, Exception, String, bool, bool> errParams;
        //Pop an error off the queue and deal with it:
        ErrorStack.TryPop(out errParams);
        HandleError(errParams.Item2, errParams.Item3, errParams.Item4, errParams.Item5);
    }
}
//Dispatches a call to HandleError on the UI thread.
private static void Invoke_HandleError(Exception ex, string extraInfo, bool showMsgBox, bool resetApplication)
{
    ((App)App.Current).Dispatcher.BeginInvoke((Action<Exception, string, bool, bool>)
        delegate(Exception _ex, string _extraInfo, bool _showMsgBox, bool _resetApplication)
        {
            ErrorHandled(_ex); //Release the semaphore taken by the spawning HandleError call
            HandleError(_ex, _extraInfo, _showMsgBox, _resetApplication);
        }, DispatcherPriority.Background, new object[] { ex, extraInfo, showMsgBox, resetApplication });
}