【问题标题】:Threading with WinForms?使用 WinForms 进行线程化?
【发布时间】:2011-11-25 10:39:27
【问题描述】:

在我的应用程序中,我启动了一个隐藏的dummyForm,它只是为了跟踪主 UI 线程而创建的。因此,如果即将创建一个新表单 InvokeRequired 在虚拟表单上使用,以确保我们在创建新表单时位于主 UI 线程上。

在实例化我的frmStart 表单后直接检查frmStart.InvokeRequired 并将其设置为false,因此无需在此处调用(dummyForm.InvokeRequired 也是如此)。

然后我得到了一个frmMyDialog,它将使用frmStart 作为父母/所有者,如下所示:

using(Create frmMyDialog on main UI thread)
{
    frmMyDialog.Show(frmStart);
}

这会抛出一个跨线程异常,这里奇怪的是:

frmMyDialog.InvokeRequired = false
dummyForm.InvokeRequired = false
frmStart.InvokeRequired = true

即使我在创建frmStart 时检查dummyForm.InvokeRequired 是否为假?

frmMyDialog.InvokeRequired 的值应始终与dummyForm.InvokeRequired 相同?这里发生了什么?

我检查了frmStartdummyForm 在创建第一个实例后根本没有重新创建。

编辑1:

这是应用程序的启动方式:

public static void Main(string[] args)
{
     _instance = new MyClientMain(parameters);
     Application.Run(_instance);
}

MyClientMain 类的构造函数将在名为 MainControl 的静态类上运行安装程序。 MainControler 将在 setup 方法中实例化一个像这样的虚拟表单:

if (_dummyForm == null)
   _dummyForm = new Form();

完成此操作后,登录表单将处理登录,并且此表单是多线程的。登录完成后,将再次调用 MainController 以实例化并打开包含 frmStart 的主 MDI 窗口。为确保我们在同一个线程上,请执行以下操作:

public static StartApplication()
{
if (_dummyForm.InvokeRequired)
                    _dummyForm.Invoke(new MethodInvoker(delegate { OpenMainOrbitWindow(); }));
     //Instanciate mainform and frmStart then open mainForm with frmStart as a MDI child

}

这里没有多线程。

然后,当服务离线时,将触发一个事件,我需要弹出一个 frmMyDialog,但是当使用 .ShowDialog() 时,它的对话框将放置在表单后面,因此最能找到父/所有者并设置如下:

public static Form GetActiveForm()
        {
            Form activeForm = Form.ActiveForm;

            if (activeForm != null)
                return activeForm;

            if (MainOrbitForm.TopMost)
                return MainOrbitForm;
            else
            {
                FormCollection openForms = Application.OpenForms;
                for (int i = 0; i < openForms.Count && activeForm == null; ++i)
                {
                    Form openForm = openForms[i];
                    if (openForm.IsMdiContainer)
                        return openForm.ActiveMdiChild;
                }
            }

            if (_patientForm != null)
            {
                if (_patientForm.TopMost)
                    return _patientForm;
            }

            return null;
        }

        public static string ShowOrbitDialogReName()
        {
            frmMyDialog myDialog;
            Form testForm;

            //Makes sures that the frmOrbitDialog is created with the same thread as the dummyForm
            //InvokeRequired is used for this
            using (myDialog = MainController.CreateForm<frmOrbitDialog>())
            {
               //Settings...
               testForm = GetActiveForm();
               myDialog.ShowDialog(GetActiveForm(testForm));


            }
        }

问题是

myDialog.InvokeRequired = false
testForm.InvokeRequired = true;
MainController.DummyForm.InvokeRequired = false;

编辑2: 启动并创建虚拟表单:

dummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 9 

登录成功后我们创建主窗体

_mainForm.InvokeRequired = false
MainControl.DummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 9

到目前为止,一切看起来都很好。然后收到一个回调(WCF),一个事件在同一个线程上创建一个 frmMyDialog(在 dummyForm 上使用 Invoke),然后使用 ShowDialog:

frmMyCustomDialog.ShowDialog(_mainForm)

这会引发 CrossThreadException,这就是它现在的样子:

_mainForm.InvokeRequired = true
frmMyCustomDialog.InvokeRequired = false
MainControl.DummyForm.InvokeRequired = false
Thread.CurrentThread.ManagedThreadId = 12

为什么 MainControl.DummyForm 不正确? ManageThreadId 不是 9 而是 12?

【问题讨论】:

  • 显示更多真实代码,了解您如何创建涉及异常的表单。 InvokeRequired 未绑定到主 UI 线程,而是绑定到创建控件/表单的线程!
  • 你是否多次调用Application.Run?
  • @Jodrell 请看Edit1
  • 我脑子里有太多表格错误 - 我出去了:)
  • 如果不考虑状态而只强制调用会发生什么?

标签: c# winforms multithreading


【解决方案1】:

您应该使用System.Threading.SynchronizationContext.Current。它是为了这样的目的而直接创建的。 在创建应用程序的第一个表单后,您可以在任何地方访问它。从您下面的示例来看,这应该不是问题,因为您在应用程序开始时就创建了一个表单。

public static void Main(string[] args)
{
     _instance = new MyClientMain(parameters);
     Application.Run(_instance);
}

然后在任何地方,您需要在 UI 线程上执行代码,您只需使用

System.Threading.SynchronizationContext.Current.Send() // To execute your code synchronously
System.Threading.SynchronizationContext.Current.Post() // To execute your code synchronously

请注意,SynchronizationContext 足够聪明,您已经从 UI 线程调用它,然后它会直接执行您的委托。 另外,请记住,在第一次使用 SynchronizationContext 之前,您需要创建一些 WinForms 表单或控件,因为这样做时,上下文将被初始化为适当的实现。

有 3 种实现: 默认,什么都不做 - 只是始终同步运行代码,它保持在 Current 中,直到您创建 WinForms 控件或 WPF 控件。 然后 Current 将填充 Winforms 的上下文或 WPF 调度程序的上下文。

【讨论】:

  • 谢谢,但 System.Threading.SynchronizationContext.Current 即使在我创建并打开表单后仍为空?
  • 嗯,显然 SynchronizationContext 是每个线程的单个线程,当 frmMyDialog 即将显示时,我们似乎在另一个线程上。
  • 你必须让你的表单可见。
  • 不,问题在于并非所有线程都有 SynchronizationContext,它是每个线程的单例。当我要打开对话框时,它是另一个线程(不是创建 dummyForm 的线程,而是从 WCF 回调创建的线程),并且该线程没有 SynchronizationContext。也许我可以将主 UI 线程的 SynchronizationContext 存储在我的静态 MainController 中并使用它而不是虚拟表单,但我不确定这是一个好的解决方案吗?
  • 这是一篇关于 SynchronizationContext 的精彩文章 > codeproject.com/KB/threads/SynchronizationContext.aspx
【解决方案2】:

这只是我的想法,但正如 Vladimir Perevalov 在另一次讨论中所说,你必须让你的表单可见。

如果您的 frmDummy 从未显示,则它永远不会创建和分配它的窗口句柄,因此它总是会向“InvokeRequired”回复 False。这意味着您要通过 frmDummy 同步的所有代码实际上永远不会发送到初始 UI 线程,而是始终在当前线程中运行。 (对于刚刚创建的控件,它变成了自己的 UI 线程)。

需要注意的重要一点是 InvokeRequired 尝试确定所述控件的窗口句柄是否由另一个线程拥有。它与构造函数无关。

如果您不想显示 frmDummy,您可以在实例化它之后立即调用 CreateControl,以确保它已分配了句柄。

【讨论】:

    【解决方案3】:

    我没有完全理解你的问题,因为你的例子并没有真正展示任何关于多线程的东西——但是,如果你想创建一个表单,其中父级是另一个线程的另一个表单,你可以使用这个代码:

    public void CreateShowDialogForm()
    {
      if (this.InvokeRequired)
      {
        this.Invoke(new Action(CreateShowDialogForm));
      }
      else
      {
        Form frmMyDialog = new Form();
        frmMyDialog.Show(this);
      }
    }
    
    private void Form4_Load(object sender, EventArgs e)
    {
      Task t = new Task(() => CreateShowDialogForm());
      t.Start();
      t.ContinueWith(task => true);
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2010-12-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多