WPF 线程模型

news2024/11/26 10:00:13

Windows Presentation Foundation (WPF) 旨在将开发人员从线程处理困难中解脱出来。 因此,大多数 WPF 开发人员不会编写使用多个线程的界面。 由于多线程程序既复杂又难以调试,因此当存在单线程解决方案时,应避免使用多线程程序。

但是,无论构建得多好,没有任何 UI 框架能为每种问题都提供单线程解决方案。 WPF 虽然在这方面有近乎完美的表现,但某些情况下,仍需要使用多线程来改进用户界面 (UI) 响应能力或应用程序性能。 基于上文所述的背景材料,本文对上述情况进行探讨,然后通过对一些低级别的细节进行讨论作出总结。

备注
本主题介绍使用 InvokeAsync 方法进行异步调用的线程处理。 InvokeAsync 方法采用 Action 或 Func 作为参数,并返回具有 Task 属性的 DispatcherOperation 或 DispatcherOperation。 可以将 await 关键字与 DispatcherOperation 或相关联的 Task 配合使用。 如果需要同步等待 DispatcherOperation 或 DispatcherOperation 返回的 Task,请调用 DispatcherOperationWait 扩展方法。 调用 Task.Wait 将导致死锁。 有关使用 Task 执行异步操作的详细信息,请参阅基于任务的异步编程。
若要进行同步调用,请使用 Invoke 方法,该方法还具有采用委托、Action 或 Func 参数的重载。

概述和调度程序

通常,WPF 应用程序从两个线程开始:一个用于处理渲染,另一个用于管理 UI。 当 UI 线程接收输入、处理事件、绘制屏幕和运行应用程序代码时,呈现线程通过隐藏方式在后台高效运行。 大多数应用程序使用单个 UI 线程,不过在某些情况下,最好使用多个线程。 我们将稍后通过示例对此进行讨论。

UI 线程在称为 Dispatcher 的对象内对工作项进行排队。 Dispatcher 基于优先级选择工作项,并运行每一个工作项直到完成。 每个 UI 线程必须具有至少一个 Dispatcher,且每个 Dispatcher 都可精确地在一个线程中执行工作项。

若要生成响应迅速、用户友好的应用程序,诀窍在于通过保持工作项小型化来最大化 Dispatcher 吞吐量。 这样一来,工作项就不会停滞在 Dispatcher 队列中,因等待处理而过时。 输入和响应间任何可察觉的延迟都会让用户不满。

那么 WPF 应用程序应该如何处理大型操作呢? 如果代码涉及大型计算,或需要查询某些远程服务器上的数据库,应该怎么办? 通常情况下,解决方法是在单独的线程中处理大型操作,让 UI 线程自由地倾向于 Dispatcher 队列中的项。 大型操作完成后,它可以将其结果报告回 UI 线程以进行显示。

传统而言,Windows 允许 UI 元素仅由创造它们的线程访问。 这意味着,负责长时间运行任务的后台线程无法在任务完成时更新文本框。 Windows 这么做的目的是确保 UI 组件的完整性。 如果在绘制过程中后台线程更新了列表框的内容,则此列表框看起来可能会很奇怪。

WPF 具有内置互相排斥机制,此机制能强制执行这种协调。 WPF 中的大多数类都派生自 DispatcherObject。 构造时,DispatcherObject 会存储对 Dispatcher(它链接到当前正在运行的线程)的引用。 实际上,DispatcherObject 与创建它的线程相关联。 在程序执行期间,DispatcherObject 可以调用它的公共 VerifyAccess 方法。 VerifyAccess 检查与当前线程相关联的 Dispatcher,并将其与构造期间存储的 Dispatcher 引用相比较。 如果它们不匹配,VerifyAccess 会引发异常。 系统会在属于 DispatcherObject 的每个方法的开头调用 VerifyAccess。

如果可以修改 UI 的线程只有一个,后台线程将如何与用户进行交互? 后台线程可请求 UI 线程代表自己来执行操作。 它通过向 UI 线程的 Dispatcher 注册工作项来实现此目的。 Dispatcher 类提供了用于注册工作项的方法:Dispatcher.InvokeAsync、Dispatcher.BeginInvoke 和 Dispatcher.Invoke。 这些方法都计划一个用于执行的委托。 Invoke 是一个同步调用,也就是说,在 UI 线程真正执行完委托之前,它不会返回。 InvokeAsync 和 BeginInvoke 是异步的,并立即返回。

Dispatcher 按优先级对其队列中的元素排序。 向 Dispatcher 队列添加元素时,可以指定十个级别。 这些优先级均在 DispatcherPriority 枚举中维护。

具有长时间运行的计算的单线程应用

在等待由响应用户交互而生成的事件时,大多数图形用户界面 (GUI) 在大多数时间处于空闲状态。 通过精心编程,可建设性地使用这些空闲时间,且不会影响 UI 的响应能力。 WPF 线程模型不允许输入中断 UI 线程中发生的操作。 这意味着,必须确保定期返回 Dispatcher,以便在过时之前处理挂起的输入事件。

演示本部分概念的适用于 C# 或 Visual Basic 的示例应用可从 GitHub 下载。
请考虑以下示例:
在这里插入图片描述

这个简单的应用程序从 3 开始向上计数以搜索质数。 用户单击“开始”按钮时,开始执行搜索。 当程序查找到一个质数时,它将根据其发现内容更新用户界面。 用户可随时停止搜索。

尽管十分简单,但对质数的搜索可以永远持续下去,这会带来一些问题。 如果在按钮的单击事件处理程序中处理整个搜索,UI 线程将永远没有机会处理其他事件。 UI 将无法响应输入,也无法处理消息。 它将永远不会重绘,也永远不会响应按钮单击。

可以在单独的线程中搜索质数,但这样的话,我们需要处理一些同步问题。 通过单线程方法,可以直接更新列出所找到的最大质数的标签。

如果将计算任务分解为可管理的多个区块,则可以定期返回 Dispatcher,并处理事件。 WPF 就有机会重绘和处理输入。

划分计算和事件处理之间的处理时间的最佳方式是从 Dispatcher 管理计算。 通过使用 InvokeAsync 方法,可以在从中绘制 UI 事件的同一队列中计划质数检查。 在我们的示例中,一次仅计划一个质数检查。 完成质数检查后,立即计划下一个检查。 仅当处理挂起的 UI 事件后,此检查才会继续。

在这里插入图片描述
Microsoft Word 通过此机制完成拼写检查。 拼写检查是在后台利用 UI 线程的空闲时间完成的。 我们来看一看代码。

下列示例显示了创建用户界面的 XAML。

<Window x:Class="SDKSamples.PrimeNumber"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Prime Numbers" Width="360" Height="100">
    <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
        <Button Content="Start"  
                Click="StartStopButton_Click"
                Name="StartStopButton"
                Margin="5,0,5,0" Padding="10,0" />
        
        <TextBlock Margin="10,0,0,0">Biggest Prime Found:</TextBlock>
        <TextBlock Name="bigPrime" Margin="4,0,0,0">3</TextBlock>
    </StackPanel>
</Window>
using System;
using System.Windows;
using System.Windows.Threading;

namespace SDKSamples
{
    public partial class PrimeNumber : Window
    {
        // Current number to check
        private long _num = 3;
        private bool _runCalculation = false;

        public PrimeNumber() =>
            InitializeComponent();

        private void StartStopButton_Click(object sender, RoutedEventArgs e)
        {
            _runCalculation = !_runCalculation;

            if (_runCalculation)
            {
                StartStopButton.Content = "Stop";
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
            }
            else
                StartStopButton.Content = "Resume";
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            _isPrime = true;

            for (long i = 3; i <= Math.Sqrt(_num); i++)
            {
                if (_num % i == 0)
                {
                    // Set not a prime flag to true.
                    _isPrime = false;
                    break;
                }
            }

            // If a prime number, update the UI text
            if (_isPrime)
                bigPrime.Text = _num.ToString();

            _num += 2;
            
            // Requeue this method on the dispatcher
            if (_runCalculation)
                StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
        }

        private bool _isPrime = false;
    }
}

除更新 Button 上的文本外,StartStopButton_Click 处理程序还负责通过向 Dispatcher 队列添加委托,计划首个质数检查。 在此事件处理程序完成其工作后一段时间,Dispatcher 将选择用于执行的委托。

如前文所述,InvokeAsync 是用于计划委托执行的 Dispatcher 成员。 在这种情况下,选择 SystemIdle 优先级。 仅当没有要处理的重要事件时,Dispatcher 才会执行此委托。 UI 响应能力比数字检查更重要。 我们还传递了一个表示数字检查例程的新委托。

public void CheckNextNumber()
{
    // Reset flag.
    _isPrime = true;

    for (long i = 3; i <= Math.Sqrt(_num); i++)
    {
        if (_num % i == 0)
        {
            // Set not a prime flag to true.
            _isPrime = false;
            break;
        }
    }

    // If a prime number, update the UI text
    if (_isPrime)
        bigPrime.Text = _num.ToString();

    _num += 2;
    
    // Requeue this method on the dispatcher
    if (_runCalculation)
        StartStopButton.Dispatcher.InvokeAsync(CheckNextNumber, DispatcherPriority.SystemIdle);
}

private bool _isPrime = false;

此方法检查下一个奇数是否是质数。 如果是质数,此方法将直接更新 bigPrimeTextBlock,以反映此发现。 可以如此操作的原因是,该计算发生在用于创建控件的相同线程中。 如果选择使用单独的线程来进行计算,将必须使用更复杂的同步机制,并在 UI 线程中执行更新。 我们将在下一步中演示这种情况。

多窗口、多线程

某些 WPF 应用程序要求多个顶层窗口。 通过单个线程/Dispatcher 组合来管理多个窗口是完全可以接受的,但有时多线程可以做得更好。 尤其当这些窗口中的某一个将有可能要独占线程时,更是如此。

Windows 资源管理器以这种方式工作。 每个新资源管理器窗口都属于原始进程,但它是在独立线程的控件下创建的。 当资源管理器变得非响应时(例如在查找网络资源时),其他资源管理器窗口将继续响应且可用。

可以使用以下示例演示此概念。
在这里插入图片描述
此示例包含一个带以下内容的窗口:旋转的 ‼️ 字形、一个“暂停”按钮和另外两个用于在当前线程下或新线程中创建新窗口的按钮。 ‼️ 字形不断旋转,直到按下“暂停”按钮,该按钮会将线程暂停五秒。 在窗口的底部,将显示线程标识符。

当按下“暂停”按钮时,同一线程下的所有窗口都变得无响应。 不同线程下的任何窗口将继续正常工作。

以下示例是窗口的 XAML:

<Window x:Class="SDKSamples.MultiWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="Thread Hosted Window" Width="360" Height="180" SizeToContent="Height" ResizeMode="NoResize" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*" />
            <RowDefinition Height="Auto" />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <TextBlock HorizontalAlignment="Right" Margin="30,0" Text="‼️" FontSize="50" FontWeight="ExtraBold"
                   Foreground="Magenta" RenderTransformOrigin="0.5,0.5" Name="RotatedTextBlock">
            <TextBlock.RenderTransform>
                <RotateTransform Angle="0" />
            </TextBlock.RenderTransform>
            <TextBlock.Triggers>
                <EventTrigger RoutedEvent="Loaded">
                    <BeginStoryboard>
                        <Storyboard>
                            <DoubleAnimation Storyboard.TargetName="RotatedTextBlock"
                                Storyboard.TargetProperty="(UIElement.RenderTransform).(RotateTransform.Angle)"
                                From="0" To="360" Duration="0:0:5" RepeatBehavior="Forever" />
                        </Storyboard>
                    </BeginStoryboard>
                </EventTrigger>
            </TextBlock.Triggers>
        </TextBlock>

        <StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="20" >
            <Button Content="Pause" Click="PauseButton_Click" Margin="5,0" Padding="10,0" />
            <TextBlock Margin="5,0,0,0" Text="<-- Pause for 5 seconds" />
        </StackPanel>

        <StackPanel Grid.Row="1" Margin="10">
            <Button Content="Create 'Same Thread' Window" Click="SameThreadWindow_Click" />
            <Button Content="Create 'New Thread' Window" Click="NewThreadWindow_Click" Margin="0,10,0,0" />
        </StackPanel>

        <StatusBar Grid.Row="2" VerticalAlignment="Bottom">
            <StatusBarItem Content="Thread ID" Name="ThreadStatusItem" />
        </StatusBar>

    </Grid>
</Window>
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;

namespace SDKSamples
{
    public partial class MultiWindow : Window
    {
        public MultiWindow() =>
            InitializeComponent();

        private void Window_Loaded(object sender, RoutedEventArgs e) =>
            ThreadStatusItem.Content = $"Thread ID: {Thread.CurrentThread.ManagedThreadId}";

        private void PauseButton_Click(object sender, RoutedEventArgs e) =>
            Task.Delay(TimeSpan.FromSeconds(5)).Wait();

        private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
            new MultiWindow().Show();

        private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
        {
            Thread newWindowThread = new Thread(ThreadStartingPoint);
            newWindowThread.SetApartmentState(ApartmentState.STA);
            newWindowThread.IsBackground = true;
            newWindowThread.Start();
        }

        private void ThreadStartingPoint()
        {
            new MultiWindow().Show();

            System.Windows.Threading.Dispatcher.Run();
        }
    }
}

以下是要注意的一些详细信息:

  • Task.Delay(TimeSpan) 任务用于在按下“暂停”按钮时使当前线程暂停五秒钟。
private void PauseButton_Click(object sender, RoutedEventArgs e) =>
    Task.Delay(TimeSpan.FromSeconds(5)).Wait();
  • SameThreadWindow_Click 事件处理程序立即在当前线程下显示一个新窗口。 NewThreadWindow_Click 事件处理程序创建一个新线程,该线程开始执行 ThreadStartingPoint 方法,该方法又显示一个新窗口,如下一个要点所述。
private void SameThreadWindow_Click(object sender, RoutedEventArgs e) =>
    new MultiWindow().Show();

private void NewThreadWindow_Click(object sender, RoutedEventArgs e)
{
    Thread newWindowThread = new Thread(ThreadStartingPoint);
    newWindowThread.SetApartmentState(ApartmentState.STA);
    newWindowThread.IsBackground = true;
    newWindowThread.Start();
}
  • ThreadStartingPoint 方法是新线程的起点。 新窗口是在此线程的控制下创建的。 WPF 自动创建新的 System.Windows.Threading.Dispatcher 来管理新线程。 若要使窗口功能化,我们要做的是启动 System.Windows.Threading.Dispatcher。
private void ThreadStartingPoint()
{
    new MultiWindow().Show();

    System.Windows.Threading.Dispatcher.Run();
}

使用 Task.Run 处理阻塞操作

在图形应用程序中处理阻塞操作可能很困难。 我们不希望从事件处理程序调用阻塞方法,因为应用程序已经冻结。 前面的示例在其自己的线程中创建了新窗口,让每个窗口彼此独立运行。 虽然我们可以使用 System.Windows.Threading.Dispatcher 创建一个新线程,但工作完成后很难将新线程与主 UI 线程同步。 由于新线程无法直接修改 UI,因此我们必须使用 Dispatcher.InvokeAsync、Dispatcher.BeginInvoke 或 Dispatcher.Invoke 将委托插入到 UI 线程的 Dispatcher 中。 最终,将通过可修改 UI 元素的权限来执行这些委托。

有一种更简单的方法可以在新线程上运行代码并同步结果,即基于任务的异步模式 (TAP)。 它基于 System.Threading.Tasks 命名空间中的 Task 和 Task 类型,这些类型用于表示异步操作。 TAP 使用单个方法表示异步操作的开始和完成。 此模式有一些好处:

  • Task 的调用方可以选择异步或同步运行代码。
  • Task 可以报告进度。
  • 调用代码可以暂停执行并等待操作的结果。

Task.Run 示例

在本例中,我们模拟了一个检索天气预报的远程过程调用。 单击该按钮时,UI 将更新为指示正在提取数据,同时开始模拟一个提取天气预报的任务。 启动任务后,按钮事件处理程序代码将暂停,直到任务完成。 任务完成后,事件处理程序代码将继续运行。 代码暂停,不会阻止 UI 线程的其余部分。 WPF 的同步上下文处理暂停代码,从而允许 WPF 继续运行。

在这里插入图片描述
演示本部分概念的适用于 C# 或 Visual Basic 的示例应用可从 GitHub 下载。 此示例的 XAML 非常大,本文未提供。 使用前面的 GitHub 链接浏览 XAML。 XAML 使用单个按钮提取天气。

using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Threading.Tasks;

namespace SDKSamples
{
    public partial class Weather : Window
    {
        public Weather() =>
            InitializeComponent();

        private async void FetchButton_Click(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

            // Asynchronously fetch the weather forecast on a different thread and pause this code.
            string weather = await Task.Run(FetchWeatherFromServerAsync);

            // After async data returns, process it...
            // Set the weather image
            if (weather == "sunny")
                weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

            else if (weather == "rainy")
                weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

            //Stop clock animation
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
            ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
            
            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;
        }

        private async Task<string> FetchWeatherFromServerAsync()
        {
            // Simulate the delay from network access
            await Task.Delay(TimeSpan.FromSeconds(4));

            // Tried and true method for weather forecasting - random numbers
            Random rand = new Random();

            if (rand.Next(2) == 0)
                return "rainy";
            
            else
                return "sunny";
        }

        private void HideClockFaceStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowWeatherImageStoryboard"]).Begin(ClockImage);

        private void HideWeatherImageStoryboard_Completed(object sender, EventArgs args) =>
            ((Storyboard)Resources["ShowClockFaceStoryboard"]).Begin(ClockImage, true);
    }
}

以下是一些需要注意的详细信息。

按钮事件处理程序

private async void FetchButton_Click(object sender, RoutedEventArgs e)
{
    // Change the status image and start the rotation animation.
    fetchButton.IsEnabled = false;
    fetchButton.Content = "Contacting Server";
    weatherText.Text = "";
    ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

    // Asynchronously fetch the weather forecast on a different thread and pause this code.
    string weather = await Task.Run(FetchWeatherFromServerAsync);

    // After async data returns, process it...
    // Set the weather image
    if (weather == "sunny")
        weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

    else if (weather == "rainy")
        weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

    //Stop clock animation
    ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
    ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
    
    //Update UI text
    fetchButton.IsEnabled = true;
    fetchButton.Content = "Fetch Forecast";
    weatherText.Text = weather;
}

请注意,事件处理程序已声明为 async(或 Visual Basic 的 Async)。 “异步”方法允许在调用等待的方法(例如 FetchWeatherFromServerAsync)时暂停代码。 这是由 await(或 Visual Basic 的 Await)关键字指定的。 在 FetchWeatherFromServerAsync 完成之前,按钮的处理程序代码将暂停,并将控件返回到调用方。 这类似于同步方法,不同之处在于,同步方法等待方法中的每个操作完成,之后控件将返回到调用方。

等待的方法利用当前方法的线程上下文,对于按钮处理程序为 UI 线程。 这意味着调用 await FetchWeatherFromServerAsync();(或 Visual Basic 的 Await FetchWeatherFromServerAsync())会导致 FetchWeatherFromServerAsync 中的代码在 UI 线程上运行,但不在有时间运行该代码的调度程序上执行,与具有长时间运行的计算的单线程应用示例的操作方式类似。 但是,请注意使用了 await Task.Run。 这会在线程池中为指定任务创建一个新线程(而不是当前线程)。 因此 FetchWeatherFromServerAsync 在自己的线程上运行。

  • 获取天气
private async Task<string> FetchWeatherFromServerAsync()
{
    // Simulate the delay from network access
    await Task.Delay(TimeSpan.FromSeconds(4));

    // Tried and true method for weather forecasting - random numbers
    Random rand = new Random();

    if (rand.Next(2) == 0)
        return "rainy";
    
    else
        return "sunny";
}

为简便起见,本例中没有任何网络代码。 通过使新线程进入休眠状态四秒钟,模拟网络访问的延迟。 此时,原始 UI 线程仍在运行并响应 UI 事件,而按钮的事件处理程序一直处于暂停状态,直到新线程完成。 为了演示这一点,我们让动画继续运行,你可以调整窗口大小。 如果 UI 线程已暂停或延迟,则不会显示动画,并且你无法与窗口交互。

Task.Delay 完成后,我们已随机选择天气预报,天气状态将返回到调用方。

  • 更新 UI
private async void FetchButton_Click(object sender, RoutedEventArgs e)
{
    // Change the status image and start the rotation animation.
    fetchButton.IsEnabled = false;
    fetchButton.Content = "Contacting Server";
    weatherText.Text = "";
    ((Storyboard)Resources["HideWeatherImageStoryboard"]).Begin(this);

    // Asynchronously fetch the weather forecast on a different thread and pause this code.
    string weather = await Task.Run(FetchWeatherFromServerAsync);

    // After async data returns, process it...
    // Set the weather image
    if (weather == "sunny")
        weatherIndicatorImage.Source = (ImageSource)Resources["SunnyImageSource"];

    else if (weather == "rainy")
        weatherIndicatorImage.Source = (ImageSource)Resources["RainingImageSource"];

    //Stop clock animation
    ((Storyboard)Resources["ShowClockFaceStoryboard"]).Stop(ClockImage);
    ((Storyboard)Resources["HideClockFaceStoryboard"]).Begin(ClockImage);
    
    //Update UI text
    fetchButton.IsEnabled = true;
    fetchButton.Content = "Fetch Forecast";
    weatherText.Text = weather;
}

当任务完成并且 UI 线程有时间时,按钮的事件处理程序 Task.Run 的调用方将继续。 该方法的其余部分停止时钟动画,并选择一个图像来描述天气。 它显示此图像并启用“提取预测”按钮。

技术详细信息和疑难点

以下部分介绍了多线程处理时可能会遇到的一些详细信息和疑难点。

嵌套泵

有时无法完全锁定 UI 线程。 让我们考虑一下 MessageBox 类的 Show 方法。 在用户单击“确定”按钮之前,Show 不会返回。 但是,它却会创建一个窗口,该窗口为了获得交互性而必须具有消息循环。 在等待用户单击“确定”时,原始应用程序窗口将不会响应用户的输入。 但是,它将继续处理绘制消息。 当被覆盖和被显示时,原始窗口将重绘其本身。

在这里插入图片描述
一些线程必须负责消息框窗口。 WPF 可以为消息框窗口创建新线程,但此线程无法在原始窗口中绘制禁用的元素(请回忆之前所讨论的互相排斥)。 WPF 使用嵌套消息处理系统。 Dispatcher 类包括一个名为 PushFrame 的特殊方法,它存储应用程序的当前执行点,然后启动一个新的消息循环。 当嵌套消息循环结束后,将在原始 PushFrame 调用之后继续执行。

在此情况下,PushFrame 将在调用 MessageBox.Show 时维护程序上下文,并且它将启动一个新的消息循环,用于重绘后台窗口,并处理对消息框窗口的输入。 当用户单击“确定”并清除弹出窗口时,嵌套循环将退出,并在调用 Show 后继续控制。

过时的路由事件

引发事件时,WPF 中的路由事件系统会通知整个树。

<Canvas MouseLeftButtonDown="handler1" 
        Width="100"
        Height="100"
        >
  <Ellipse Width="50"
            Height="50"
            Fill="Blue" 
            Canvas.Left="30"
            Canvas.Top="50" 
            MouseLeftButtonDown="handler2"
            />
</Canvas>

在椭圆形上按下鼠标左键时,将执行 handler2。 handler2 完成后,事件将传递到 Canvas 对象,后者使用 handler1 对其进行处理。 仅当 handler2 没有显式标记事件对象为已处理时,才会发生这种情况。

handler2 可能会花费大量时间来处理此事件。 handler2 可能使用 PushFrame 来启动嵌套消息循环,并在数小时内不会返回任何内容。 如果在此消息循环完成时,handler2 尚未将事件标记为已处理,该事件将沿树向上传递(即使它很旧)。

重新进入和锁定

公共语言运行时 (CLR) 的锁定机制与人们所设想的完全不同;可能有人以为在请求锁定时,线程将完全停止操作。 实际上,该线程将继续接收和处理高优先级的消息。 这样有助于防止死锁,并使接口最低限度地响应,但这样做有可能引入细微 bug。 绝大多数时间里,你无需知晓有关这点的任何情况,但在极少数情况下(通常涉及 Win32 窗口消息或 COM STA 组件),可能需要知道这一点。

大部分接口在生成过程中并未考虑线程安全问题,这是因为开发人员在开发过程中假定 UI 绝不会由一个以上的线程访问。 在此情况下,该单个线程可能在意外情况下更改环境,造成不良影响,这些影响应由 DispatcherObject 互相排斥机制来解决。 请看下面的伪代码:

在这里插入图片描述
大多数情况下这都没有问题,但在某些时候 WPF 中的异常重入确实会造成严重问题。 因此在某些关键时刻,WPF 调用 DisableProcessing,这会更改该线程的锁定指令,以使用 WPF 无重入锁定,而非常规 CLR 锁定。

那么,为何 CLR 团队选择这种行为? 它与 COM STA 对象和完成线程有关。 在对一个对象进行垃圾回收时,其 Finalize 方法运行在专用终结器线程之上,而非 UI 线程上。 这其中就存在问题,因为在 UI 线程上创建的 COM STA 对象只能在 UI 线程上释放。 CLR 相当于 BeginInvoke(在此例中使用 Win32 的 SendMessage)。 但如果 UI 线程正忙,终结器线程被停止,COM STA 对象无法被释放,这将造成严重的内存泄漏。 因此,CLR 团队通过严格的调用,使锁定以这种方式工作。

WPF 的任务是在不重新引入内存泄漏的情况下,避免异常的重入,因此我们不阻止各个位置的重入。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1190817.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Docker安装RockeMQ:基于Windows宿主机,并重点解决docker rocketMQ安装情况下控制台无法访问的问题】

拉取镜像 docker pull rocketmqinc/rocketmq创建网络 docker network create rocketmq-net构建namesrv容器 docker run -d -p 9876:9876 -v D:/dockerFile/rocketmq/namesrv/logs:/root/logs -v D:/dockerFile/rocketmq/namesrv/store:/root/store --network rocketmq-net -…

11.9存储器实验总结(单ram,双ram,FIFO)

实验设计 单端口RAM实现 双端口RAM实现 FIFO实现 文件结构为

【Royalty in Wind 2.0.0】个人体测计算、资料分享小程序

前言 Royalty in Wind 是我个人制作的一个工具类小程序。主要涵盖体测计算器、个人学习资料分享等功能。这个小程序在2022年第一次发布&#xff0c;不过后来因为一些原因暂时搁置。现在准备作为我个人的小程序重新投入使用XD PS&#xff1a;小程序开发部分我是在21年跟随郄培…

使命担当 守护安全 | 中睿天下获全国海关信息中心感谢信

近日&#xff0c;全国海关信息中心向中睿天下发来感谢信&#xff0c;对中睿天下在2023年网络攻防演练专项活动中的大力支持和优异表现给予了高度赞扬。 中睿天下对此次任务高度重视&#xff0c;紧密围绕全国海关信息中心的行动要求&#xff0c;发挥自身优势有效整合资源&#x…

Spring Boot 3.0正式发布及新特性解读

目录 【1】Spring Boot 3.0正式发布及新特性依赖调整升级的关键变更支持 GraalVM 原生镜像 Spring Boot 最新支持版本Spring Boo 版本版本 3.1.5前置系统清单三方包升级 Ref 个人主页: 【⭐️个人主页】 需要您的【&#x1f496; 点赞关注】支持 &#x1f4af; 【1】Spring Boo…

GUI:贪吃蛇

以上是准备工作 Data import javax.swing.*; import java.net.URL;public class Data {public static URL headerURLData.class.getResource("static/header.png");public static ImageIcon header new ImageIcon(headerURL);public static URL upURLData.class.getR…

【树的存储结构,孩子链表】

文章目录 树和森林树的存储结构孩子链表 树和森林 森林&#xff1a;是m(m>0)棵互不相交的树的集合。 树的存储结构 1.双亲表示法 实现&#xff1a;定义结构数组存放树的结点&#xff0c;每个结点含两个域。 数据域&#xff1a;存放结点本身信息。 双亲域&#xff1a;指…

如何设计vue项目的权限管理?

权限管理的重要性及必要性 数据安全&#xff1a;权限管理可以确保只有具有相应权限的用户能够访问和操作特定的数据。这可以保护敏感数据不被未授权的用户访问&#xff0c;从而提高数据的安全性。功能控制&#xff1a;权限管理可以根据用户的角色和权限设置&#xff0c;控制用户…

Ansible自动化运维工具(常用模块与命令)

ansible基于Python开发&#xff0c;实现了批量系统配置&#xff0c;批量程序部署&#xff0c;批量运行命令等功能 ansible特点 部署简单&#xff0c;只需在主控端部署Ansible环境&#xff0c;被控端无需做任何操作&#xff1b;默认使用ssh协议对设备进行管理&#xff1b;有大…

SPASS-交叉表分析

导入数据 修改变量测量类型 分析->描述统计->交叉表 表中显示行、列变量通过卡方检验给出的独立性检验结果。共使用了三种检验方法。上表各种检验方法显著水平sig.都远远小于0.05,所以有理由拒绝实验准备与评价结果是独立的假设&#xff0c;即认为实验准备这个评价指标是…

DehazeNet: An End-to-End System for Single Image Haze Removal(端到端的去雾模型)

1、论文去雾总体思路 DehazeNet是2016年华南理工大学的研究者提出的一个端到端的深度学习模型&#xff0c;该模型主要通过输入的原始有雾图像拟合出该图所对应的medium transmission map&#xff08;透射率t值图&#xff09;&#xff0c;并使用引导滤波对t值进行refine&#x…

SpringCloud - OpenFeign 参数传递和响应处理(全网最详细)

目录 一、OpenFeign 参数传递和响应处理 1.1、feign 客户端参数传递 1.1.1、零散类型参数传递 1. 例如 querystring 方式传参 2. 例如路径方式传参 1.1.2、对象参数传递 1. 对象参数传递案例 1.1.3、数组参数传递 1. 数组传参案例 1.1.4、集合类型的参数传递&#xf…

PHP+MySQL人才招聘小程序系统源码 带完整前端+后端搭建教程

在当今竞争激烈的人才市场中&#xff0c;招聘平台的需求日益增长。传统的招聘平台往往需要投入大量的人力物力进行维护和管理&#xff0c;这对于许多中小企业来说是一个沉重的负担。因此&#xff0c;开发一个简单易用、高效便捷的招聘平台显得尤为重要。 PHP是一种流行的服务器…

通过docker-compose部署elk日志系统,并使用springboot整合

ELK是一种强大的分布式日志管理解决方案&#xff0c;它由三个核心组件组成&#xff1a; Elasticsearch&#xff1a;作为分布式搜索和分析引擎&#xff0c;Elasticsearch能够快速地存储、搜索和分析大量的日志数据&#xff0c;帮助用户轻松地找到所需的信息。 Logstash&#xf…

逐次变分模态分解(Sequential Variational Mode Decomposition,SVMD)(附代码)

代码原理 逐次变分模态分解&#xff08;Sequential Variational Mode Decomposition&#xff0c;SVMD&#xff09;是一种用于信号处理和数据分析的方法。它可以将复杂的信号分解为一系列模态函数&#xff0c;每个模态函数代表了信号中的一个特定频率成分。SVMD的主要目标是提取…

【每日一题】咒语和药水的成功对数

文章目录 Tag题目来源解题思路方法一&#xff1a;排序二分 写在最后 Tag 【排序二分】【数组】【2023-11-10】 题目来源 2300. 咒语和药水的成功对数 解题思路 方法一&#xff1a;排序二分 我们首先对 points 进行升序排序&#xff0c;然后枚举 spells 中的 x&#xff0c;需…

持续集成交付CICD:安装Gitlab Runner(从节点)

目录 一、实验 1.选择Gitlab Runner版本 2.安装Gitlab Runner&#xff08;第一种方式&#xff1a;交互式安装&#xff09; 3.安装Gitlab Runner&#xff08;第二种方式&#xff1a;非交互式安装&#xff09; 二、问题 1.如何查看Gitlab版本 一、实验 1.选择Gitlab Runne…

如何用Excel软件制作最小二乘法①

一、用自带的选项&#xff08;不推荐&#xff09;&#xff0c;因为感觉只是近似&#xff0c;虽然结果一样 1.在Excel中输入或打开要进行在excel中输入或打开要进行最小二乘法拟合的数据&#xff0c;如图所示。 2.按住“shift”键的同时&#xff0c;用鼠标左键单击以选择数据&a…

android手机平板拓展电脑音频

&#xff08;1&#xff09;首先确保电脑上有声卡&#xff0c;就是电脑右下角小喇叭能调音量&#xff0c;不管电脑会不会响&#xff0c;如果小喇叭标记了个错误&#xff0c;说明没有声卡&#xff0c;安装图上的虚拟声卡软件。 &#xff08;2&#xff09;图上第一个PC免安装及局…

Code Review最佳实践

Code Review最佳实践 Code Review 我一直认为Code Review&#xff08;代码审查&#xff09;是软件开发中的最佳实践之一&#xff0c;可以有效提高整体代码质量&#xff0c;及时发现代码中可能存在的问题。包括像Google、微软这些公司&#xff0c;Code Review都是基本要求&…