【问题标题】:Program hangs after leaving screen saver or locking computer离开屏幕保护程序或锁定计算机后程序挂起
【发布时间】:2011-12-29 19:00:52
【问题描述】:

我们的程序运行良好,直到有人锁定计算机或弹出屏幕保护程序(但不是 ctrl+alt+delete)。一旦计算机解锁/屏幕保护程序关闭,应用程序将停止绘制除标题栏以外的所有内容,并停止响应输入 - 它显示一个无法移动或关闭的大部分白色窗口。

(应用程序冻结示例 - 山脉来自我的桌面背景)

如果我们让它静置大约 5~10 分钟,它就会恢复活力,并且不会再次挂起(即使在锁定计算机/屏幕保护程序弹出窗口之后),直到应用程序重新启动.

很难调试,因为从Visual Studio启动程序时不会发生,只有手动打开.exe时才会发生。

只有在启动画面显示时才会发生 - 如果我删除代码以显示启动画面,它就会停止发生。但是,我们需要启动画面。

我已经尝试了this page的所有建议;唯一不会发生这种情况的是使用Microsoft.VisualBasic.WindowsFormsApplicationBase,但这会导致各种其他问题。

互联网上有关此的信息似乎很少 - 以前有没有人遇到过类似的问题?


以下是相关代码:

//Multiple programs use this login form, all have the same issue
public partial class LoginForm<TMainForm>
    where TMainForm : Form, new()
{
    private readonly Action _showLoadingForm;

    public LoginForm(Action showLoadingForm)
    {
        ...
        _showLoadingForm = showLoadingForm;
    }

    private void btnLogin_Click(object sender, EventArgs e)
    {
        ...
        this.Hide();
        ShowLoadingForm(); //Problem goes away when commenting-out this line
        new TMainForm().ShowDialog();
        this.Close();
    }

    private void ShowLoadingForm()
    {
        Thread loadingFormThread = new Thread(o => _showLoadingForm());
        loadingFormThread.IsBackground = true;
        loadingFormThread.SetApartmentState(ApartmentState.STA);
        loadingFormThread.Start();
    }
}

以下是其中一个程序中使用的_showLoadingForm 操作之一的示例:

public static bool _showSplash = true;
public static void ShowSplashScreen()
{
    //Ick, DoEvents!  But we were having problems with CloseSplashScreen being called
    //before ShowSplashScreen - this hack was found at
    //https://stackoverflow.com/questions/48916/multi-threaded-splash-screen-in-c/48946#48946
    using(SplashForm splashForm = new SplashForm())
    {
        splashForm.Show();
        while(_showSplash)
            Application.DoEvents();
        splashForm.Close();
    }
}

//Called in MainForm_Load()
public static void CloseSplashScreen()
{
    _showSplash = false;
}

【问题讨论】:

  • 调试器锁定时能不能附加?
  • 仔细看看它挂在哪里。是真的在MainForm.ShowDialog 内部,还是在调用堆栈中更具体的东西。如果它真的被困在ShowDialog 中,那么这意味着消息不再被泵送到主 UI 线程上。这将是非常奇怪的,并且可能表明 .NET 中存在一个仅在显示初始屏幕时才会出现的模糊错误。这是一个很奇怪的问题。
  • 顺便说一下,调用Application.DoEvents 的while 循环应该会不停地旋转。是否消耗大量 CPU 时间?
  • _showSplash 应该是volatile,顺便说一句。
  • 如果你没有看到它没有任何意义。这种代码经常会遇到 SystemEvents 类的问题。你可以混淆它,让它在错误的线程上触发事件。

标签: c# vb.net winforms multithreading freeze


【解决方案1】:

因为没有工作示例

您可以尝试删除 Application.DoEvents(); 并插入 thread.sleep 吗?

Application.DoEvents(); 可以说是非常邪恶的。

【讨论】:

  • 正如我所说,我在this page 上尝试了所有其他建议。没有创建启动画面的方法有效,所以不是因为DoEvents()
【解决方案2】:

启动画面问题

DoEvents 事情是非常不可取的,并且不一定能完成您认为它所做的事情。 DoEvents 告诉 CLR 处理 windows 消息循环(用于启动屏幕),但不一定为其他线程提供任何处理时间。 Thread.Sleep() 将为其他线程提供处理的机会,但不一定允许启动屏幕的 Windows 消息循环继续发送消息。因此,如果您必须使用循环,那么您确实需要两者,但稍后我将建议您完全摆脱此循环。除了那个循环问题之外,我没有看到任何明确的方式来清理启动线程。你需要某种Thread.Join()Thread.Abort() 发生在某处。

我喜欢使用 ManualResetEvent 来同步启动表单和调用线程,而不是使用 Application.DoEvents() 循环。这样,ShowSplash() 方法在显示启动画面之前不会返回。在那之后的任何时候,我们显然都可以关闭它,因为我们知道它已经完成显示。

这里有几个很好的例子:.NET Multi-threaded Splash Screens in C#

以下是我修改@AdamNosfinger 发布的我最喜欢的示例的方法,以包含一个ManualResetEvent 以将ShowSplash 方法与启动屏幕线程同步:

public partial class FormSplash : Form
{
    private static Thread _splashThread;
    private static FormSplash _splashForm;
    // This is used to make sure you can't call SplashScreenClose before the SplashScreenOpen has finished showing the splash initially.
    static ManualResetEvent SplashScreenLoaded;

    public FormSplash()
    {
        InitializeComponent();

        // Signal out ManualResetEvent so we know the Splash form is good to go.
        SplashScreenLoaded.Set();
    }

    /// <summary>
    /// Show the Splash Screen (Loading...)
    /// </summary>
    public static void ShowSplash()
    {
        if (_splashThread == null)
        {
            // Setup our manual reset event to syncronize the splash screen thread and our main application thread.
            SplashScreenLoaded = new ManualResetEvent(false);

            // show the form in a new thread
            _splashThread = new Thread(new ThreadStart(DoShowSplash));
            _splashThread.IsBackground = true;
            _splashThread.Start();

            // Wait for the splash screen thread to let us know its ok for the app to keep going. 
            // This next line will not return until the SplashScreen is loaded.
            SplashScreenLoaded.WaitOne();
            SplashScreenLoaded.Close();
            SplashScreenLoaded = null;
        }
    }

    // called by the thread
    private static void DoShowSplash()
    {
        if (_splashForm == null)
            _splashForm = new FormSplash();

        // create a new message pump on this thread (started from ShowSplash)
        Application.Run(_splashForm);
    }

    /// <summary>
    /// Close the splash (Loading...) screen
    /// </summary>
    public static void CloseSplash()
    {
        // need to call on the thread that launched this splash
        if (_splashForm.InvokeRequired)
            _splashForm.Invoke(new MethodInvoker(CloseSplash));

        else
            Application.ExitThread();
    }
}

主表单问题

看起来好像您正在使用 ShowDialog 从登录窗口启动主窗体,然后关闭登录窗体。我理解正确了吗?如果是这样,那就不好了。 ShowDialog 适用于您的应用程序的子窗口,并希望拥有一个所有者窗口,如果您未在方法参数中指定所有者窗体,则假定当前活动窗口是所有者。见MSDN

所以你的主表单假设登录表单是它的父表单,但是你在显示主表单后不久就关闭了登录表单。所以我不确定应用程序当时处于什么状态。您应该考虑使用标准的Form.Show() 方法,如果这是所需的结果(例如:BorderStyle、MaximizeBox、MinimizeBox、ControlBox、TopMost),只需调整 Form 属性以显示为对话框。

重要编辑:好吧,我是人类,我搞砸了,忘记 ShowDialog 是一种阻塞方法。虽然这确实否定了所有者句柄问题,但我仍然建议不要将 ShowDialog 用于您的主应用程序表单,除非您可以为其提供与外观或线程无关的重要理由(因为应该使用其他技术修复这些理由)。尽管我犯了错误,但这个建议仍然有效。

可能的绘画问题

您没有指定您正在使用哪些控件,或者您是否在您的应用程序中进行任何自定义绘画。但是您需要记住,当您锁定计算机时,某些窗口句柄会被强制关闭。例如,如果您有一些自定义绘制控件并且正在缓存字体、画笔或其他 GDI 资源,则您需要在代码中包含一些 try { ... } catch { ... } 块,以便在绘制期间引发异常时处理并重建缓存的 GDI 资源。我在自定义绘制列表框并缓存一些 GDI 对象之前遇到过这种情况。如果您在应用程序的任何位置(包括启动画面)中有任何自定义绘画代码,请仔细检查所有 GDI 对象是否已妥善处理/清理。

【讨论】:

  • 请重新阅读这个问题——我不仅说我已经尝试过其他显示启动表单的方法,而且我明确地链接到了完全相同的线程!然而,这些方法都不能解决问题。这不是自定义绘画问题,因为该程序不会在任何用户代码中挂起(也如上所述)。而且您似乎将Form.ShowDialog()Form.Show() 混淆了-ShowDialog() 在子表单退出之前不会返回,因此在程序准备退出之前不会调用this.Close()
  • 您对 ShowDialog 被阻塞的看法是正确的,我的错误在于 - 我将在那里编辑一些更正。但是为什么还要使用 ShowDialog 以及为什么要保留登录表单呢?我确实阅读了您链接到的参考帖子,您会注意到我修改了该问题中我最喜欢的答案,以包含一个阻塞 ShowSplash 函数,因为您说您在显示前调用关闭时遇到问题。我正在为您已经尝试过的内容添加更多代码,以便通过快速启动解决可能的订购问题。
【解决方案3】:

从我对您的代码所做的快速扫描来看,您的问题的关键可能正在使用

Application.Run(_splashForm);

理想情况下,您会在线程中使用它,但也许它也可以与您的 DoEvents 一起使用。对不起,如果你这样做了,我只是错过了......

【讨论】:

    【解决方案4】:

    在上面的sn-ps代码中添加几行代码后,我就可以编译出一个工作程序了。但是,我无法重现该问题(Windows 7 Starter)。我尝试锁定计算机,并启动屏幕保护程序。我在启动画面处于活动状态时执行此操作,在其他情况下,但主窗口始终保持响应。我认为这里肯定有其他事情发生,可能是在主窗口的初始化期间。

    这是代码,也许它可以帮助其他人找出问题。

    using System;
    using System.Threading;
    using System.Windows.Forms; 
    
    public class MainForm : Form
    {
      //Here is an example of one of the _showLoadingForm actions used in one of the programs:
      public static bool _showSplash = true;
      public static void ShowSplashScreen()
      {
        //Ick, DoEvents!  But we were having problems with CloseSplashScreen being called
        //before ShowSplashScreen - this hack was found at
        //http://stackoverflow.com/questions/48916/multi-threaded-splash-screen-in-c/48946#48946
        using(SplashForm splashForm = new SplashForm())
        {
          splashForm.Show();
          while(_showSplash)
            Application.DoEvents();
          splashForm.Close();
        }
      }
    
      //Called in MainForm_Load()
      public static void CloseSplashScreen()
      {
        _showSplash = false;
      }
    
      public MainForm() 
      { 
        Text = "MainForm"; 
        Load += delegate(object sender, EventArgs e) 
        {
          Thread.Sleep(3000);
          CloseSplashScreen(); 
        };
      }
    }
    
    //Multiple programs use this login form, all have the same issue
    public class LoginForm<TMainForm> : Form where TMainForm : Form, new()
    {
      private readonly Action _showLoadingForm;
    
      public LoginForm(Action showLoadingForm)
      {
        Text = "LoginForm";
        Button btnLogin = new Button();
        btnLogin.Text = "Login";
        btnLogin.Click += btnLogin_Click;
        Controls.Add(btnLogin);
        //...
        _showLoadingForm = showLoadingForm;
      }
    
      private void btnLogin_Click(object sender, EventArgs e)
      {
        //...
        this.Hide();
        ShowLoadingForm(); //Problem goes away when commenting-out this line
        new TMainForm().ShowDialog();
        this.Close();
      }
    
      private void ShowLoadingForm()
      {
        Thread loadingFormThread = new Thread(o => _showLoadingForm());
        loadingFormThread.IsBackground = true;
        loadingFormThread.SetApartmentState(ApartmentState.STA);
        loadingFormThread.Start();
      }
    }
    
    public class SplashForm : Form
    {
      public SplashForm() 
      { 
        Text = "SplashForm"; 
      }
    }
    
    public class Program
    {
      public static void Main()
      {
        var loginForm = new LoginForm<MainForm>(MainForm.ShowSplashScreen);
        loginForm.Visible = true;
        Application.Run(loginForm);
      }
    }
    

    【讨论】:

      【解决方案5】:

      在我们的应用程序中,闪屏出现了一些类似的问题。我们想要一个带有动画 gif 的闪屏(不要怪我,这是一个管理决定)。只有当 splashScreen 有自己的消息循环时,它才能正常工作。因为我认为DoEvents 是解决您问题的关键,所以我向您展示了我们是如何解决它的。希望它能帮助您解决问题!

      我们将以这种方式显示初始屏幕:

      // AnimatedClockSplashScreen is a special form from us, it can be any other!
      // Our form is set to be TopMost
      splashScreen = new AnimatedClockSplashScreen(); 
      Task.Factory.StartNew(() => Application.Run(splashScreen));
      

      启动画面是一个简单的包含时钟动画的 gif。它没有任何循环,所以它不会随时变钢。

      当需要关闭splash时,我们是这样操作的:

      if (splashScreen != null)
      {
          if (splashScreen.IsHandleCreated)
          {
              try
              {
                  splashScreen.Invoke(new MethodInvoker(() => splashScreen.Close()));
              }
              catch (InvalidOperationException)
              {
              }
          }
          splashScreen.Dispose();
          splashScreen = null;
      }
      

      【讨论】:

        【解决方案6】:

        删除这一行,你不需要它,当默认为 mta 时,你将它强制为单个线程。取默认值。

        loadingFormThread.SetApartmentState(ApartmentState.STA);
        

        更改以下内容:

        using(SplashForm splashForm = new SplashForm())
        {
            splashForm.Show();
            while(_showSplash)
                Application.DoEvents();
            splashForm.Close();
        }
        

        到:

        SplashForm splashForm = new SplashForm())
        splashForm.Show();
        

        改变这个:

        public static void CloseSplashScreen()
        {
            _showSplash = false;
        }
        

        到这里:

        public static void CloseSplashScreen()
        {
            splashForm.Close();
        }
        

        【讨论】:

          【解决方案7】:

          您是否尝试过使用 WaitHandle 在线程中显示表单?

          类似:

          EventWaitHandle _waitHandle = new AutoResetEvent(false);
          public static void ShowSplashScreen()
          {
              using(SplashForm splashForm = new SplashForm())
              {
                  splashForm.Show();
                  _waitHandle.WaitOne();
                  splashForm.Close();
              }
          }
          
          //Called in MainForm_Load()
          public static void CloseSplashScreen()
          {
              _waitHandle.Set();
          }
          

          【讨论】:

            【解决方案8】:

            这是一个黑暗中的镜头:当我们空闲时,我们也要求线程进入睡眠状态。我不确定这是否会有所帮助,但值得一试:

                while(_showSplash) {
                    System.Threading.Thread.Sleep(500);
                    Application.DoEvents();
                }
            

            【讨论】:

              【解决方案9】:

              我认为您的问题是因为您使用的是Form.ShowDialog,而不是Application.Run。 ShowDialog 运行一个受限消息循环,该循环在主消息循环之上运行并忽略一些 Windows 消息。

              这样的事情应该可以工作:

              static class Program
              {
                  /// <summary>
                  /// The main entry point for the application.
                  /// </summary>
                  [STAThread]
                  static void Main()
                  {
                      Application.EnableVisualStyles();
                      Application.SetCompatibleTextRenderingDefault( false );
              
                      Application.Run( new MainForm() );
                  }
              }
              
              
              public partial class MainForm: Form
              {
                  FormSplash dlg = null;
              
                  void ShowSplashScreen()
                  {
                      var t = new Thread( () =>
                          {
                              using ( dlg = new FormSplash() ) dlg.ShowDialog();
                          }
                      );
              
                      t.SetApartmentState( ApartmentState.STA );
                      t.IsBackground = true;
                      t.Start();
                  }
              
                  void CloseSplashScreen()
                  {
                      dlg.Invoke( ( MethodInvoker ) ( () => dlg.Close() ) );
                  }
              
                  public MainForm()
                  {
                      ShowSplashScreen();
              
                      InitializeComponent();
              
                      Thread.Sleep( 3000 ); // simulate big form
              
                      CloseSplashScreen();
                  }
              }
              

              【讨论】:

                【解决方案10】:

                几年后(代码不再摆在我面前),我将为遇到此问题的其他人添加答案。


                问题结果与Hans Passant had guessed 完全相同。问题是,由于 .Net 框架中的一些非常模糊和无害的错误,InvokeRequired 有时会在应该返回 true 时返回 false,导致应该在 GUI 线程上运行的代码在后台运行(由于一些更模糊和无害的错误,导致我看到的行为)

                解决方案是不要依赖InvokeRequired,使用类似这样的hack:

                void Main()
                {
                    Thread.Current.Name = "GuiThread";
                    ...
                }
                
                bool IsGuiThread()
                {
                    return Thread.Current.Name == "GuiThread";
                }
                
                //Later, call IsGuiThread() to determine if GUI code is being run on GUI thread
                

                here 找到了这个解决方案,以及对问题原因的极其深入的了解。

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 2022-12-21
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2021-09-18
                  • 2020-07-04
                  相关资源
                  最近更新 更多