18

我们正在开发一个 WPF 应用程序,它将同时打开多个报表(就像典型的 MDI 应用程序,如 Excel 或 Visual Studio)。尽管可以让这些报表的数据上下文在多个工作线程中运行,但我们仍然发现,如果打开的报表数量真的很大,即使是这些报表的呈现(基本上 UserControl 托管在 MDI 环境中或仅在主视图中的网格区域)仍然会使应用程序的响应速度降低。

所以,我的想法是在主 UI 中至少有几个区域,每个区域都有其用户控件在不同的 UI 线程中运行。再次,想象一下 Visual Studio 中的一个典型视图,除了菜单之外,它具有文本编辑器的主要区域、托管例如解决方案资源管理器的侧面区域和托管例如错误列表和输出的底部区域。所以我希望这三个区域在三个 UI 线程中运行(但自然它们托管在一个 MainView 中,这是我不确定的部分)。

我问是因为我知道可以在不同的 UI 线程中运行多个(顶级)窗口。但是有人说它不适用于用户控件。这是真的吗?如果是这样,我的场景的典型解决方案是什么,即打开的 UserControl 的数量非常大,而且其中许多 UserControl 是实时的,因此渲染它们会占用大量资源?谢谢!

4

3 回答 3

24

UI 线程模型的背景信息

通常,应用程序有一个“主”UI 线程......它可能有 0 个或多个后台/工作者/非 UI 线程,您(或 .NET 运行时/框架)在其中执行后台工作。

(...WPF 中有另一个特殊线程,称为渲染线程,但我现在将跳过它...)

例如,一个简单的 WPF 应用程序可能具有以下线程列表:

在此处输入图像描述

一个简单的 WinForms 应用程序可能有这个线程列表:

在此处输入图像描述

当您创建一个元素时,它与特定的Dispatcher& 线程绑定(具有亲和力),并且只能从与Dispatcher.

如果您尝试从不同的线程访问对象的属性或方法,通常会出现异常,例如在 WPF 中:

在此处输入图像描述

在 Windows 窗体中:

在此处输入图像描述

对 UI 的任何修改都需要在创建 UI 元素的同一线程上执行……因此后台线程用于Invoke/BeginInvoke在 UI 线程上运行该工作。

演示在非 UI 线程上创建元素的问题

<Window x:Class="WpfApplication9.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525" Loaded="Window_Loaded">
    <StackPanel x:Name="mystackpanel">

    </StackPanel>
</Window>

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.ComponentModel;
using System.Threading;
using System.Windows.Threading;

namespace WpfApplication9
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        Thread m_thread1;
        Thread m_thread2;
        Thread m_thread3;
        Thread m_thread4;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            CreateAndAddElementInDifferentWays();
        }

        void CreateAndAddElementInDifferentWays()
        {
            string text = "created in ui thread, added in ui thread [Main STA]";
            System.Diagnostics.Debug.WriteLine(text);

            CreateAndAddTextChild(text);

            // Do NOT use any Joins with any of these threads, otherwise you will get a
            // deadlock on any "Invoke" call you do.

            // To better observe and focus on the behaviour when creating and
            // adding an element from differently configured threads, I suggest
            // you pick "one" of these and do a recompile/run.

            ParameterizedThreadStart paramthreadstart1 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnThread);
            m_thread1 = new Thread(paramthreadstart1);
            m_thread1.SetApartmentState(ApartmentState.STA);
            m_thread1.Start("[STA]");

            //ParameterizedThreadStart paramthreadstart2 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnUIThread);
            //m_thread2 = new Thread(paramthreadstart2);
            //m_thread2.SetApartmentState(ApartmentState.STA);
            //m_thread2.Start("[STA]");

            //ParameterizedThreadStart paramthreadstart3 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnThread);
            //m_thread3 = new Thread(paramthreadstart3);
            //m_thread3.SetApartmentState(ApartmentState.MTA);
            //m_thread3.Start("[MTA]");

            //ParameterizedThreadStart paramthreadstart4 = new ParameterizedThreadStart(this.WorkCreatedOnThreadAddedOnUIThread);
            //m_thread4 = new Thread(paramthreadstart4);
            //m_thread4.SetApartmentState(ApartmentState.MTA);
            //m_thread4.Start("[MTA]");
        }

        //----------------------------------------------------------------------

        void WorkCreatedOnThreadAddedOnThread(object parameter)
        {
            string threadingmodel = parameter as string;

            string text = "created in worker thread, added in background thread, " + threadingmodel;
            System.Diagnostics.Debug.WriteLine(text);

            CreateAndAddTextChild(text);
        }

        void WorkCreatedOnThreadAddedOnUIThread(object parameter)
        {
            string threadingmodel = parameter as string;

            string text = "created in worker thread, added in ui thread via invoke" + threadingmodel;
            System.Diagnostics.Debug.WriteLine(text);

            TextBlock tb = CreateTextBlock(text);
            if (tb != null)
            {
                // You can alternatively use .Invoke if you like!

                DispatcherOperation dispop = Dispatcher.BeginInvoke(new Action(() =>
                {
                    // Get this work done on the main UI thread.

                    AddTextBlock(tb);
                }));

                if (dispop.Status != DispatcherOperationStatus.Completed)
                {
                    dispop.Wait();
                }
            }
        }

        //----------------------------------------------------------------------

        public TextBlock CreateTextBlock(string text)
        {
            System.Diagnostics.Debug.WriteLine("[CreateTextBlock]");

            try
            {
                TextBlock tb = new TextBlock();
                tb.Text = text;
                return tb;
            }
            catch (InvalidOperationException ex)
            {
                // will always exception, using this to highlight issue.
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }

            return null;
        }

        public void AddTextBlock(TextBlock tb)
        {
            System.Diagnostics.Debug.WriteLine("[AddTextBlock]");

            try
            {
                mystackpanel.Children.Add(tb);
            }
            catch (InvalidOperationException ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.Message);
            }
        }

        public void CreateAndAddTextChild(string text)
        {
            TextBlock tb = CreateTextBlock(text);
            if (tb != null)
                AddTextBlock(tb);
        }
    }
}

辅助 UI 线程又名“在另一个线程上创建顶级窗口”

可以创建辅助 UI 线程,只要将线程标记为使用 STA 单元模型,并创建一个Dispatcher(例如 use Dispatcher.Current)并启动一个“运行”循环(Dispatcher.Run()),以便Dispatcher可以为创建的 UI 元素提供消息那个线程。

但是在一个 UI 线程中创建的元素不能放入另一个在不同 UI 线程上创建的元素的逻辑/视觉树中。

混合在不同 UI 线程上创建的元素的解决方法

有一种有限的解决方法,它可以为您提供一些能力,可以将在一个 UI 线程中创建的元素的呈现与在不同线程中创建的可视化树组合在一起......通过使用HostVisual. 看这个例子:

于 2012-09-12T13:14:12.597 回答
1

不,UserControl 与 UI 线程相关联。即使您能够在其他地方初始化它们,将它们添加到主 UI 时也会遇到问题,因为它们属于不同的线程。

于 2012-09-12T12:21:31.590 回答
0

您可以将可视化树的渲染拆分到不同的线程中。

请参阅这篇文章以获得良好的解释和呈现视频输出的示例。 http://blogs.msdn.com/b/dwayneneed/archive/2007/04/26/multithreaded-ui-hostvisual.aspx

但是,只有在 Visual 的实际渲染在其他地方实现或者在 WPF 应用程序中(例如将 Direct3D 场景渲染为 Visual)在技术上非常奇怪时,这样做才是真正合理的。

正如文章中提到的,这里的重要说明是,如果辅助线程呈现 WPF XAML,那么您会丢失输入事件,因为路由事件不能跨越线程边界。

于 2014-10-10T13:33:13.727 回答