【问题标题】:C# Multi threading- Move objects between threadsC# 多线程 - 在线程之间移动对象
【发布时间】:2010-06-01 23:36:02
【问题描述】:

我正在使用一个 winforms 控件,它既是一个 GUI 元素,也执行一些尚未向开发人员公开的内部处理。当这个组件被实例化时,它可能需要 5 到 15 秒才能准备好,所以我想做的是把它放在另一个线程上,当它完成后把它带回 gui 线程并将它放在我的表单上。问题是这将(并且已经)导致跨线程异常。

通常,当我使用工作线程时,它只使用简单的数据对象,我可以在处理完成后推回,然后使用主线程上已经存在的控件,但我从来不需要以这种方式移动整个控件。

有谁知道这是否可行,如果可以,怎么办?如果不是,如何处理这样一个有可能锁定主 gui 的问题?

【问题讨论】:

  • 除了控件本身之外,初始化工作是否可以分离到自己的线程中?
  • 你好罗伯特很遗憾不是。
  • 如何中断对“ControlContext”的初始化,在线程中初始化上下文并将其发送回控件,实际上是从 Context.Properties 设置 Control.Properties?
  • 您好 Patrick,不幸的是,初始化与控件紧密耦合。
  • (你可以通过一些反射和非常讨厌不安全的代码来解决这个问题,但我每次看到它都会哭泣,而且我肯定不会想要任何人将该代码归还给我。)

标签: c# winforms multithreading


【解决方案1】:

你不需要锁定GUI,你只需要调用invoke:

Windows 窗体中的控件绑定到 一个特定的线程并且不是线程 安全的。因此,如果您正在调用 不同的控制方法 线程,您必须使用其中一种 控件的调用方法来编组 调用正确的线程。这 属性可用于确定是否 您必须调用一个调用方法,该方法 如果你不知道什么会很有用 线程拥有一个控件。 ref

这是它在代码中的样子:

public delegate void ComponentReadyDelegate(YourComponent component);
public void LoadComponent(YourComponent component)
{
    if (this.InvokeRequired)
    {
        ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
        this.BeginInvoke(e, new object[]{component});
    }
    else
    {
        // The component is used by a UI control
        component.DoSomething();
        component.GetSomething();
    }
}

// From the other thread just initialize the component
// and call the LoadComponent method on the GUI.
component.Initialize(); // 5-15 seconds
yourForm.LoadComponent(component);

通常从另一个线程调用LoadComponent 会导致跨线程异常,但通过上述实现,该方法将在 GUI 线程上调用。

InvokeRequired 告诉你:

调用者必须调用一个调用方法 当对 控制,因为调用者在 与那个不同的线程 控件创建于。 ref

更新:
因此,如果我理解正确,控制对象是在 GUI 线程以外的线程上创建的,因此即使您能够将其传递给 GUI 线程,您仍然无法在不导致跨线程异常的情况下使用它.解决方案是在 GUI 线程上创建对象,但在单独的线程上对其进行初始化:

public partial class MyForm : Form
{
    public delegate void ComponentReadyDelegate(YourComponent component);
    private YourComponent  _component;
    public MyForm()
    {
        InitializeComponent();
        // The componet is created on the same thread as the GUI
        _component = new YourComponent();

        ThreadPool.QueueUserWorkItem(o =>
        {
            // The initialization takes 5-10 seconds
            // so just initialize the component in separate thread
            _component.Initialize();

            LoadComponent(_component);
        });
    }

    public void LoadComponent(YourComponent component)
    {
        if (this.InvokeRequired)
        {
            ComponentReadyDelegate e = new ComponentReadyDelegate(LoadComponent);
            this.BeginInvoke(e, new object[]{component});
        }
        else
        {
            // The component is used by a UI control
            component.DoSomething();
            component.GetSomething();
        }
    }
}

【讨论】:

  • 这不会遇到 O.P. 想要避免的同样问题吗?也就是说,他想在后台初始化他的控件,这样它就不会阻塞 GUI 线程,因为初始化需要 15 秒。如果 Invoke 用于将执行编组回 GUI 线程,那不会仍然阻塞 GUI 线程吗?
  • @Tim,我假设当 OP 调用 yourForm.LoadComponent 时组件已经在单独的线程中初始化。 GUI 不会被初始化负担。
  • 嗨,我不确定我是否说清楚了。使用代表和后台工作人员并不能正常工作,因为他们所做的只是传回对我的对象的引用。该对象仍在工作线程上,gui线程无法使用它。
  • @Grant,在GUI线程上创建对象并在工作线程上初始化(因为你说初始化需要5-10秒)。考虑到这一点,我已经更新了我的答案。
  • @Grant 如果你通过 BeginInvoke 初始化组件,那么它会导致初始化发生在 UI 线程上,因为它是在 UI 线程上创建的……我才意识到这个问题。试图想一个解决方法。这是一个吸引人的问题。
【解决方案2】:

在不了解对象的情况下。为避免跨线程异常,您可以使初始线程调用调用(即使您是从线程调用)。

从我自己的一个应用程序中复制并粘贴:

 private delegate void UpdateStatusBoxDel(string status);

    private void UpdateStatusBox(string status)
    {
        listBoxStats.Items.Add(status);
        listBoxStats.SelectedIndex = listBoxStats.Items.Count - 1;
        labelSuccessful.Text = SuccessfulSubmits.ToString();
        labelFailed.Text = FailedSubmits.ToString();
    }

    private void UpdateStatusBoxAsync(string status)
    {
        if(!areWeStopping)
            this.BeginInvoke(new UpdateStatusBoxDel(UpdateStatusBox), status);
    }

所以基本上线程任务将调用“异步”方法。然后它将告诉主窗体开始调用(实际上是异步本身)。

我相信可能有一种更短的方法可以完成所有这些操作,而无需创建委托和两种不同的方法。但这种方式在我心中根深蒂固。这就是微软书籍教给你的东西:p

【讨论】:

    【解决方案3】:

    BackgroundWorker 类正是为这种情况而设计的。它将为您管理线程,并让您启动线程以及取消线程。该线程可以将事件发送回 GUI 线程以进行状态更新或完成。这些状态和完成事件的事件处理程序位于主 GUI 线程中,并且可以更新您的 WinForm 控件。并且 WinForm 不会被锁定。这是你需要的一切。 (在 WPF 和 Silverlight 中也同样有效。)

    【讨论】:

    • 嗨,我不确定我是否说清楚了。使用代表和后台工作人员并不能正常工作,因为他们所做的只是传回对我的对象的引用。该对象仍在工作线程上,gui线程无法使用它。
    • backgroundworker 对象可以向 GUI 线程发送进度消息,并且可以在消息中包含一个对象。这将使对象可用于 GUI 线程。
    【解决方案4】:

    控件必须从 UI 线程创建和修改,这是没有办法的。

    为了在进行长时间运行的初始化时保持 UI 响应,请将进程保持在后台线程上并调用任何控制访问。 UI 应该保持响应,但如果没有,您可以向后台线程添加一些等待时间。这是一个示例,使用 .Net 4 并行工具:http://www.lovethedot.net/2009/01/parallel-programming-in-net-40-and_30.html

    如果在初始化完成之前不允许与正在初始化的特定控件交互,则隐藏或禁用它直到完成。

    【讨论】:

    • 嗨 Ragocrazy,我没有关注。如果控件是在 UI 线程(主线程)上创建和修改的 - 我怎样才能将进程保持在后台线程上??
    • 尝试将您的工作分成不直接影响 UI 和 UI 更新的处理,然后使用 BeginInvoke(或 Invoke)进行 UI 更新。这样,所有可以在后台线程上完成的处理,你只在必要时编组到 UI 线程。因此,例如,将项目添加到列表框——所有从数据库中检索、文本格式化等都可以在后台线程上完成,然后只有 Add() 必须使用 BeginInvoke。 UI 的其余部分仍会响应用户(如果您将其设为可见,列表框也会响应)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-05-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多