Visual Studio Debugger的线程问题

时间:2009-08-07 00:54:04

标签: visual-studio debugging multithreading

我有一个旨在通过网络运行的应用程序。这意味着此应用程序的初始运行可能需要一段时间。所以我一直在制作一个启动画面来完成整个过程。

它使用线程通过静态方法显示表单。我仍然是一个线程新手,所以当我遇到错误时,我对于什么和为什么感到困惑。

当我在Visual Studio调试器外部运行时,我的代码完全没问题。但是当我从调试器内部运行它时,我得到了异常:

“跨线程操作无效:控制''从其创建的线程以外的线程访问。”

这是我的班级:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Threading;

namespace MyApp
{
    public partial class SplashScreen : Form
    {
        private static double OPACITY_INCREMENT = .05;

        private static SplashScreen _splashForm;
        private static SplashScreen SplashForm
        {
            get { return SplashScreen._splashForm; }
            set { SplashScreen._splashForm = value; }
        }

        private static void ShowForm()
        {
            SplashScreen.SplashForm = new SplashScreen();            
            Application.Run(SplashScreen.SplashForm);
        }

        internal static void CloseForm()
        {
            if (SplashScreen.SplashForm != null &&
                SplashScreen.SplashForm.IsDisposed == false)
            {
                // Make it start going away.
                OPACITY_INCREMENT = -OPACITY_INCREMENT;
            }

            SplashScreen.SplashThread = null;
            SplashScreen.SplashForm = null;
        }

        private static Thread _splashThread;
        private static Thread SplashThread
        {
            get { return SplashScreen._splashThread; }
            set { SplashScreen._splashThread = value; }
        }

        internal static void ShowSplashScreen()
        {
            if (SplashScreen.SplashForm != null)
            {
                return;
            }

            SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
            SplashScreen.SplashThread.IsBackground = true;
            SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
            SplashScreen.SplashThread.Start();
        }

        public SplashScreen()
        {
            InitializeComponent();
            this.timerFade.Start();
            this.ClientSize = this.BackgroundImage.Size;
        }

        private void SplashScreen_Load(object sender, EventArgs e)
        {
        }

        private void timerFade_Tick(object sender, EventArgs e)
        {
            if (OPACITY_INCREMENT > 0)
            {
                if (this.Opacity < 1)
                    this.Opacity += OPACITY_INCREMENT;
            }
            else
            {
                if (this.Opacity > 0)
                    this.Opacity += OPACITY_INCREMENT;
                else
                {
                    this.Invoke(new MethodInvoker(this.TryClose));
                }
            }
        }

        private void TryClose()
        {
            if (this.InvokeRequired)
            {
                this.BeginInvoke(new MethodInvoker(this.TryClose));
            }

            this.Close();
        }
    }
}

我正在从Program.cs Main方法中调用启动画面。

namespace CIMA
{
    static class Program
    {
// <summary>
        ///     The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main(string[] args)
        {            
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);

            SplashScreen.ShowSplashScreen();

            // Code omitted for brevity.

            SplashScreen.CloseForm();

            // Code omitted for brevity.
        }
    }
}

我希望能够从我的其他形式中调用SplashScreen.CloseForm(),但我还没有尝试过。我对调试器的用途感到困惑。

我错过了什么吗?或者如果在调试器中运行它,我是否必须禁用启动屏幕?

如果是这样,这样做的好方法是什么?我想尽可能避免使用编译符号,因为我讨厌跟踪它们。

3 个答案:

答案 0 :(得分:4)

在.NET中,不允许从未创建控件的线程访问控件。在您的代码中,它看起来像一个单独的线程正在创建启动窗体,主线程尝试关闭启动窗体。

我建议你反过来做:在主线程中创建splash表单并让单独的线程更新它。

此外,在我的TryClose方法中,在我调用BeginInvoke之后,回复丢失了。

但是,您可以查看启动画面的其他实现。有一些现成的,例如http://www.codeproject.com/KB/cs/prettygoodsplashscreen.aspx

答案 1 :(得分:1)

好的 - 这是另一个解决方案。我想现在就是这样。我只是包括它以防其他人出现同样的问题。简短版本:托尔斯滕是对的。

问题在于我的启动画面的关闭功能。我使用表单内部的计时器触发了飞溅表单上的淡出。计时器会将表单的不透明度递减为零,然后在达到零时关闭启动表单。

为了解决这个问题,我做了一些事情:

  1. 将原来的timerFade重命名为timerFadeIn
  2. 在Splash表单上创建了一个新的Timer:timerFadeOut
  3. 修改了我的SpalshScreen静态方法,以便在与Program.Main方法相同的线程上创建表单本身。
  4. 安排静态方法,以便从Program.Main方法调用timerFadeOut.Start()。
  5. 在不透明度下降步骤以及最终关闭方法的timerFadeOut_Tick事件处理程序中使用Invoke。
  6. 完成这些更改后,“启动画面”屏幕将按预期工作。嗯......几乎。在主窗体启动之后,新的启动屏幕才会开始淡出 - 主线程需要获得足够的处理时间,计时器才能真正开始完成其工作。但这实际上是一件好事 - 我的意思是改变现状,使主要表单加载后,Splash屏幕会消失,所以结果是加分。

    以下代码感兴趣。此外,我仍然有可能错误地实现了这种跨线程行为 - 我邀请批评。正如Kyle建议的那样,我想从良好的线程习惯开始,而不是糟糕的习惯。

    对于任何不想要FadeOut效果的人,我在代码中注释了(注释掉了)关闭表单而不使用CloseForm()方法中的计时器。

    public partial class SplashScreen : Form
    {
        private static double OPACITY_INCREMENT = .05;
    
        private static SplashScreen _splashForm;
        private static SplashScreen SplashForm
        {
            get { return SplashScreen._splashForm; }
            set { SplashScreen._splashForm = value; }
        }
    
        private static void ShowForm()
        {        
            Application.Run(SplashScreen.SplashForm);
        }
    
        internal static void CloseForm()
        {
            if (SplashScreen.SplashForm != null &&
                SplashScreen.SplashForm.IsDisposed == false)
            {
                // Make it start going away.
                SplashScreen.SplashForm.FadeOut();
            }
    
            //if (SplashScreen.SplashForm.InvokeRequired)
            //{
            //    SplashScreen.SplashForm.Invoke(new MethodInvoker(SplashScreen.SplashForm.Close));
            //}
            //else
            //{
            //    SplashScreen.SplashForm.DoClose();
            //}
    
            SplashScreen.SplashThread = null;
            SplashScreen.SplashForm = null;
        }
    
        private void FadeOut()
        {
            this.timerFadeOut.Start();
        }
    
        private static Thread _splashThread;
        private static Thread SplashThread
        {
            get { return SplashScreen._splashThread; }
            set { SplashScreen._splashThread = value; }
        }
    
        internal static void ShowSplashScreen()
        {
            if (SplashScreen.SplashForm != null)
            {
                return;
            }
    
            SplashScreen.SplashForm = new SplashScreen();
            SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
            SplashScreen.SplashThread.IsBackground = true;
            SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
            SplashScreen.SplashThread.Start();
        }
    
        public SplashScreen()
        {
            InitializeComponent();
            this.ClientSize = this.pictureBox1.Size;
        }
    
        private void SplashScreen_Load(object sender, EventArgs e)
        {
    
        }
    
        private void timerFadeIn_Tick(object sender, EventArgs e)
        {
            if (OPACITY_INCREMENT > 0)
            {
                if (this.Opacity < 1)
                {
                    this.Opacity += OPACITY_INCREMENT;
                }
                else
                {
                    this.timerFadeIn.Stop();
                }
            }
        }
    
        private void SplashScreen_Shown(object sender, EventArgs e)
        {
            this.timerFadeIn.Start();
        }
    
        private void timerFadeOut_Tick(object sender, EventArgs e)
        {
            if (OPACITY_INCREMENT > 0)
            {
                if (this.Opacity > 0)
                {
                    if (this.InvokeRequired)
                    {
                        this.Invoke(new MethodInvoker(this.FadeOutStep));
                    }
                    else
                    {
                        this.FadeOutStep();
                    }                    
                }
                else
                {
                    this.timerFadeOut.Stop();
    
                    if (this.InvokeRequired)
                    {
                        this.Invoke(new MethodInvoker(this.Close));
                    }
                    else
                    {
                        this.Close();
                    }
                }
            }
        }
    
        private void FadeOutStep()
        {
            this.Opacity -= OPACITY_INCREMENT;
        }
    }
    

答案 2 :(得分:0)

好的 - 我找到了解决方法。

System.Diagnostics.Debugger.IsAttached

我添加了以下使用声明:

using System.Diagnostics;

...并按如下方式更改了ShowSplashScreen()方法:

    internal static void ShowSplashScreen()
    {
        if (!Debugger.IsAttached)
        {
            if (SplashScreen.SplashForm != null)
            {
                return;
            }

            SplashScreen.SplashThread = new Thread(new ThreadStart(SplashScreen.ShowForm));
            SplashScreen.SplashThread.IsBackground = true;
            SplashScreen.SplashThread.SetApartmentState(ApartmentState.STA);
            SplashScreen.SplashThread.Start();
        }
    }

因此,如果我从Visual Studio运行,我的程序会忽略启动画面,或者如果没有,则运行启动画面。每次测试重建时都不再更改我的代码。

我仍然不知道为什么我在调试器内运行而不是一般代码时出错,所以我希望这个网站上的一些聪明的bug可以出现并解释一下。

否则,这种解决办法对我来说已经足够了。