c#中invokerequired偶尔会出现问题

occasionally problems with invokerequired in c#

我时不时遇到 invokerequired 方法的问题,想问问是否有人知道这个问题或者可以向我解释一下。我有一些代码:

 splashScreen splashObj = splashScreen.GetInstance();

        if (thrd == null)
        {
            thrd = new Thread(new ThreadStart(loadingScreenStart));
            thrd.IsBackground = true;
            thrd.Start();
        }

        else
        {
            if (splashObj.InvokeRequired)
            {
                splashObj.Invoke((MethodInvoker)delegate()
                {
                    splashObj.Show();
                }
                );
            }
            else
            {
                splashObj.Show();
            }
        }

        if (splashObj.loadLabel.InvokeRequired)
        {
            splashObj.loadLabel.Invoke((MethodInvoker)delegate()
            {
                splashObj.loadLabel.Text = "Checking Username...";
            }
            );
        }
        else
        {
            splashObj.loadLabel.Text = "Checking Username...";
        }


  public void loadingScreenStart()
    {
        splashScreen splashObj = splashScreen.GetInstance();
        Application.Run(splashScreen.GetInstance());
    }

启动画面代码:

 public partial class splashScreen : Form
{

    public Form1 form1;

    public splashScreen()
    {
        InitializeComponent();
        //form1 = frm1;
    }

    public splashScreen(Form1 frm1)
    {
        form1 = frm1;
    }


    private void splashScreen_Load(object sender, EventArgs e)
    {

    }

    private static splashScreen m_instance = null;
    private static object m_instanceLock = new object();

    public static splashScreen GetInstance()
    {

        lock (m_instanceLock)
        {
            if (m_instance == null)
            {
                m_instance = new splashScreen();
            }
        }

        return m_instance;
    }
}

因此,当其余代码在后台加载时,我会显示启动画面。消息显示为 "Checking Username..."、"Checking Password..."、"Loading..." 等。 这段代码工作正常,但有时似乎代码执行速度比线程快,然后我得到一个错误,该方法是从另一个线程而不是创建它的线程调用的。为什么会这样?这种问题有什么解决办法吗?这种情况可能在二十次处决中发生一次。

多线程很难,所以你应该尽量让它变得简单。第一,没有理由在启动画面 class 本身之外包含任何启动画面代码。其次,您应该始终知道自己在做什么,所以 InvokeRequired 只是一种代码味道,表示 "I have no idea who's going to call this method".

void Main()
{
    SplashScreen.ShowText("Loading 1");
    Thread.Sleep(1000);
    SplashScreen.ShowText("Loading 2");
    Thread.Sleep(2000);
    SplashScreen.Done();
    Thread.Sleep(2000);
    SplashScreen.ShowText("Loading 3");
}

// Define other methods and classes here
public partial class SplashScreen : Form
{
  private static SplashScreen instance;
  private static readonly ManualResetEvent initEvent = new ManualResetEvent(false);

  Label loadLabel;

  private SplashScreen()
  {
    // InitializeComponent();
    loadLabel = new Label();
    Controls.Add(loadLabel);

    Load += (s, e) => initEvent.Set();
    Closing += (s, e) => initEvent.Reset();
  }

  private static object syncObject = new object();
  private static void InitializeIfRequired()
  {
    // If not set, we'll have to init the message loop
    if (!initEvent.WaitOne(0))
    {
      lock (syncObject)
      {
        // Someone initialized it before us
        if (initEvent.WaitOne(0)) return;

        // Recreate the form if it was closed
        instance = new SplashScreen();

        var thread = new Thread(() => { Application.Run(instance); });
        thread.Start();

        // Wait until the form is ready
        initEvent.WaitOne();
      }
    }
  }

  public static void ShowText(string text)
  {
    InitializeIfRequired();

    instance.Invoke((Action)(() => 
     { 
       if (!instance.IsDisposed) instance.loadLabel.Text = text; 
     }
    ));
  }

  public static void Done()
  {
    // Is it closed already?
    if (!initEvent.WaitOne(0)) return;

    lock (syncObject)
    {
      // Someone closed it before us
      if (!initEvent.WaitOne(0)) return;

      instance.Invoke((Action)(() => { instance.Close(); }));
    }
  }
}

这是 LINQPad 的片段,您需要删除 InitializeComponent 以及 loadLabel 控件(应该在设计器上)的注释。

这样,您就可以将启动画面的逻辑与应用程序的其余部分完全隔离开来。要显示闪屏文本,只需调用 SplashScreen.ShowText。要使启动画面消失,请调用 SplashScreen.Done.

请注意如何不需要使用 InvokeRequired - SplashScreen.ShowText(或 Done)的任何合法调用都不会 not 需要编组到初始屏幕的 UI 线程。

现在,这并不完美。这是我在大约 10 分钟内写的东西。但它(可能 :))是线程安全的,比原来的更好地遵循最佳实践,并且更易于使用。

此外,在某些情况下我会使用更高级别的结构,例如Lazy<T>Task - 但由于这在这里并没有真正帮助(开始新的消息传递循环,如果关闭则必须重新创建表单......),我选择了更简单的解决方案。

请注意,我使用的是 Invoke 而不是 BeginInvoke 或类似的 - 这非常重要,因为否则可能会排队关闭,然后是 ShowText这将适用于处置形式。如果您打算从多个线程调用 ShowText,那么也锁定整个 ShowText 主体会更安全。考虑到启动画面的常见用例,有一些线程安全是不必要的,但是...

好的,所以这个问题仍然存在,当您的线程在显示之前调用表单时可能会发生; InvokeRequiredBeginInvoke 都可能失败。

一个解决方案是在表单事件 Shown:

上启动线程
/// <summary>
/// Testing form
/// </summary>
public partial class MyForm : Form
{
    /// <summary>
    /// Work work
    /// </summary>
    private Thread _MyThread;

    /// <summary>
    /// New test form
    /// </summary>
    public MyForm()
    {
        // Initialize components
        this.InitializeComponent();
    }

    /// <summary>
    /// First time form is show
    /// </summary>
    private void MyForm_Shown(object sender, EventArgs e)
    {
        // Create and start thread
        this._MyThread = new Thread(this.MyThreadWork);
        this._MyThread.Start();
    }

    /// <summary>
    /// Doing some work here
    /// </summary>
    private void MyThreadWork()
    {
        // Counting time
        int count = 0;

        // Forever and ever
        while (true)
        {
            this.PrintInfo((count++).ToString());
            Thread.Sleep(1000);
        }
    }

    /// <summary>
    /// Printing info into form title
    /// </summary>
    private void PrintInfo(string info)
    {
        // Needs to sync?
        if (this.InvokeRequired)
        {
            // Sync me
            this.BeginInvoke(new Action<string>(this.PrintInfo), info);
        }
        else
        {
            // Prints info
            this.Text = info;
        }
    }

    /// <summary>
    /// Good bye form, don't miss the work work
    /// </summary>
    private void MyForm_FormClosed(object sender, FormClosedEventArgs e)
    {
        // Kill worker
        this._MyThread.Abort();
    }
}

有些会在第一个 PrintInfo 之前添加一个 Thread.Sleep 但很常见,您会使用什么睡眠时间? 1、10、100?取决于您的表单显示的速度。