离开屏幕保护程序或锁定计算机后程序挂起

时间:2011-11-15 18:37:42

标签: c# vb.net winforms multithreading hang

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

Example of application freezing

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

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

调试很困难,因为只有在手动打开.exe的情况下,从Visual Studio启动程序时才会发生这种情况。

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

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

10 个答案:

答案 0 :(得分:3)

启动画面问题

DoEvents是非常不受欢迎的,并不一定能达到你的想象。 DoEvents告诉CLR参与Windows消息循环(对于启动屏幕),但不一定为其他线程提供任何处理时间。 Thread.Sleep()将为其他线程提供处理的机会,但不一定允许启动屏幕的Windows消息循环继续传送消息。所以如果你必须使用一个循环你真的需要两个,但是在一分钟之内我会建议完全远离这个循环。除了那个循环问题,我没有看到任何明确的方式正在清理启动线程。您需要在某处发生某种Thread.Join()Thread.Abort()

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

这是一个包含一些好例子的帖子:.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)。

重要编辑:好我是人类,我搞砸了,忘记了ShowDialog是一种阻止方法。虽然这确实否定了所有者处理问题,但我仍然建议不要将ShowDialog用于您的主要申请表格,除非您能为其提供非外观或线程相关的重要理由(因为这些应该通过其他技术修复)。尽管我有失误,但建议仍然合理。

可能的绘画问题

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

答案 1 :(得分:1)

在上面的代码片段中添加几行代码后,我可以编译工作程序。但是,我无法重现问题(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);
  }
}

答案 2 :(得分:1)

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

问题与Hans Passant had guessed完全相同。问题在于,由于.Net框架中存在一些令人难以置信的模糊和无害的错误,InvokeRequired有时会在返回false时返回true,从而导致应在GUI上运行的代码线程在后台运行(由于一些更加模糊和无害的错误,导致我看到的行为)

解决方案是不依赖于InvokeRequired,使用与此类似的黑客攻击:

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

答案 3 :(得分:0)

因为没有工作实例

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

Application.DoEvents(); 让我们说非常邪恶。

答案 4 :(得分:0)

通过我对您的代码进行的快速扫描,看起来您的问题的关键可能是使用

Application.Run(_splashForm);

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

答案 5 :(得分:0)

在我们的应用程序中,我们在启动屏幕上遇到了类似的问题。我们希望有一个带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动画。它没有任何循环,所以它不会随时用钢。

当需要关闭启动时,我们会这样做:

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

答案 6 :(得分:0)

删除此行,您不需要它,当默认为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 :(得分:0)

您是否尝试过使用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 :(得分:0)

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

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

答案 9 :(得分:0)

我认为您的问题是因为您使用的是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();
    }
}