【转】线程处理模型

Windows Presentation Foundation (WPF) 旨在帮助开发人员处理复杂的线程处理问题。 因此,大多数WPF开发人员不需要编写一个接口,使用多个线程。 由于多线程程序既复杂又难以调试,因此当存在单线程解决方案时,应避免使用多线程程序。

无论程度而设计,但是,否UIframework 曾经将能够为每个排序的问题提供一个单线程的解决方案。 WPF接近,但仍存在其中多个线程来提高的情况下用户界面 (UI)响应能力或应用程序的性能。 基于上文所述的背景材料,本文对上述情况进行探讨,然后通过对一些低级别的细节进行讨论作出总结。

Note

本主题讨论使用线程处理BeginInvoke异步调用的方法。 你还可以通过调用进行异步调用InvokeAsync方法,它采用ActionFunc<TResult>作为参数。 InvokeAsync方法返回DispatcherOperationDispatcherOperation<TResult>,它具有Task属性。 你可以使用await使用关键字DispatcherOperation或关联Task。 如果你需要同步等待Task返回DispatcherOperationDispatcherOperation<TResult>,调用DispatcherOperationWait扩展方法。 调用Task.Wait将导致死锁。 有关使用Task若要执行异步操作,请参阅任务并行。 Invoke方法还有采用重载ActionFunc<TResult>作为参数。 你可以使用Invoke方法来进行同步调用通过在委托中,传入ActionFunc<TResult>

概述和调度程序

通常情况下,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引发异常。 VerifyAccess用于进行调用属于每个方法的开头DispatcherObject

如果只有一个线程可以修改UI,如何执行后台线程与用户交互? 后台线程可以要求UI线程来执行其代表的操作。 这是通过注册与工作项DispatcherUI线程。 Dispatcher类提供两种方法来注册工作项:InvokeBeginInvoke。 这两种方法都计划一个用于执行的委托。 Invoke是的同步调用 – 也就是说,它不返回直到UI线程实际完成执行委托。 BeginInvoke是异步的将立即返回。

Dispatcher按优先级别的元素其队列中进行排序。 有可能时添加一个元素到指定的十个级别Dispatcher队列。 在中维护这些优先级DispatcherPriority枚举。 有关详细信息DispatcherPriority在找不到级别Windows SDK文档。

实际线程:示例

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

大多数图形用户界面 (GUI)花费大部分其时间处于空闲状态等待在响应用户交互中生成的事件。 使用小心编程时,此空闲时间可用建设性,而不会影响的响应能力UI。 WPF线程处理模型不允许输入以中断操作中发生的情况UI线程。 这意味着你必须确保以返回到Dispatcher定期到挂起的输入的事件,然后进行陈旧的进程。

请看下面的示例:

质数屏幕快照

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

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

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

如果我们分解成易于管理多个块的计算的任务时,我们可以定期返回到Dispatcher,处理的事件。 我们能否WPF重绘和处理输入的机会。

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

调度程序队列图

Microsoft Word 通过此机制完成拼写检查。 在后台使用的空闲时间执行了拼写检查UI线程。 我们来看一看代码。

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

XAML
<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Prime Numbers" Width="260" Height="75"
    >
  <StackPanel Orientation="Horizontal" VerticalAlignment="Center" >
    <Button Content="Start"  
            Click="StartOrStop"
            Name="startStopButton"
            Margin="5,0,5,0"
            />
    <TextBlock Margin="10,5,0,0">Biggest Prime Found:</TextBlock>
    <TextBlock Name="bigPrime" Margin="4,5,0,0">3</TextBlock>
  </StackPanel>
</Window>

以下示例显示了代码隐藏。

C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        public delegate void NextPrimeDelegate();
        
        //Current number to check 
        private long num = 3;   

        private bool continueCalculating = false;

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void StartOrStop(object sender, EventArgs e)
        {
            if (continueCalculating)
            {
                continueCalculating = false;
                startStopButton.Content = "Resume";
            }
            else
            {
                continueCalculating = true;
                startStopButton.Content = "Stop";
                startStopButton.Dispatcher.BeginInvoke(
                    DispatcherPriority.Normal,
                    new NextPrimeDelegate(CheckNextNumber));
            }
        }

        public void CheckNextNumber()
        {
            // Reset flag.
            NotAPrime = false;

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

            // If a prime number.
            if (!NotAPrime)
            {
                bigPrime.Text = num.ToString();
            }

            num += 2;
            if (continueCalculating)
            {
                startStopButton.Dispatcher.BeginInvoke(
                    System.Windows.Threading.DispatcherPriority.SystemIdle, 
                    new NextPrimeDelegate(this.CheckNextNumber));
            }
        }
        
        private bool NotAPrime = false;
    }
}

下面的示例演示的事件处理程序Button

C#
private void StartOrStop(object sender, EventArgs e)
{
    if (continueCalculating)
    {
        continueCalculating = false;
        startStopButton.Content = "Resume";
    }
    else
    {
        continueCalculating = true;
        startStopButton.Content = "Stop";
        startStopButton.Dispatcher.BeginInvoke(
            DispatcherPriority.Normal,
            new NextPrimeDelegate(CheckNextNumber));
    }
}

除了上更新的文本Button,此处理程序是负责计划通过将添加到委托的第一项的质数检查Dispatcher队列。 此事件处理程序完成其工作后, 一段时间内Dispatcher将选择执行此委托。

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

C#
public void CheckNextNumber()
{
    // Reset flag.
    NotAPrime = false;

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

    // If a prime number.
    if (!NotAPrime)
    {
        bigPrime.Text = num.ToString();
    }

    num += 2;
    if (continueCalculating)
    {
        startStopButton.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.SystemIdle, 
            new NextPrimeDelegate(this.CheckNextNumber));
    }
}

private bool NotAPrime = false;

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

此示例的完整源代码,请参阅长时间运行计算示例的单线程应用程序

使用后台线程处理阻塞操作

在图形应用程序中处理阻塞操作可能很困难。 我们不希望从事件处理程序调用阻塞方法,因为应用程序可能看上去冻结。 我们可以使用一个单独的线程来处理这些操作,但操作完成后,我们必须与同步UI线程因为我们不能直接修改GUI从我们的工作线程。 我们可以使用InvokeBeginInvoke要插入到的委托DispatcherUI线程。 最终,这些委托将执行有权修改UI元素。

在本例中,我们模拟了一个检索天气预报的远程过程调用。 我们使用单独的工作线程来执行此调用,并我们计划中的更新方法DispatcherUI线程在完成后。

天气 UI 屏幕快照

C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Shapes;
using System.Windows.Threading;
using System.Threading;

namespace SDKSamples
{
    public partial class Window1 : Window
    {
        // Delegates to be used in placking jobs onto the Dispatcher.
        private delegate void NoArgDelegate();
        private delegate void OneArgDelegate(String arg);

        // Storyboards for the animations.
        private Storyboard showClockFaceStoryboard;
        private Storyboard hideClockFaceStoryboard;
        private Storyboard showWeatherImageStoryboard;
        private Storyboard hideWeatherImageStoryboard;

        public Window1(): base()
        {
            InitializeComponent();
        }  

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            // Load the storyboard resources.
            showClockFaceStoryboard = 
                (Storyboard)this.Resources["ShowClockFaceStoryboard"];
            hideClockFaceStoryboard = 
                (Storyboard)this.Resources["HideClockFaceStoryboard"];
            showWeatherImageStoryboard = 
                (Storyboard)this.Resources["ShowWeatherImageStoryboard"];
            hideWeatherImageStoryboard = 
                (Storyboard)this.Resources["HideWeatherImageStoryboard"];   
        }

        private void ForecastButtonHandler(object sender, RoutedEventArgs e)
        {
            // Change the status image and start the rotation animation.
            fetchButton.IsEnabled = false;
            fetchButton.Content = "Contacting Server";
            weatherText.Text = "";
            hideWeatherImageStoryboard.Begin(this);
            
            // Start fetching the weather forecast asynchronously.
            NoArgDelegate fetcher = new NoArgDelegate(
                this.FetchWeatherFromServer);

            fetcher.BeginInvoke(null, null);
        }

        private void FetchWeatherFromServer()
        {
            // Simulate the delay from network access.
            Thread.Sleep(4000);              
            
            // Tried and true method for weather forecasting - random numbers.
            Random rand = new Random();
            String weather;

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

            // Schedule the update function in the UI thread.
            tomorrowsWeather.Dispatcher.BeginInvoke(
                System.Windows.Threading.DispatcherPriority.Normal,
                new OneArgDelegate(UpdateUserInterface), 
                weather);
        }

        private void UpdateUserInterface(String weather)
        {    
            //Set the weather image
            if (weather == "sunny")
            {       
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "SunnyImageSource"];
            }
            else if (weather == "rainy")
            {
                weatherIndicatorImage.Source = (ImageSource)this.Resources[
                    "RainingImageSource"];
            }

            //Stop clock animation
            showClockFaceStoryboard.Stop(this);
            hideClockFaceStoryboard.Begin(this);

            //Update UI text
            fetchButton.IsEnabled = true;
            fetchButton.Content = "Fetch Forecast";
            weatherText.Text = weather;     
        }

        private void HideClockFaceStoryboard_Completed(object sender,
            EventArgs args)
        {         
            showWeatherImageStoryboard.Begin(this);
        }
        
        private void HideWeatherImageStoryboard_Completed(object sender,
            EventArgs args)
        {           
            showClockFaceStoryboard.Begin(this, true);
        }        
    }
}

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

  • 创建按钮处理程序

    C#
    private void ForecastButtonHandler(object sender, RoutedEventArgs e)
    {
        // Change the status image and start the rotation animation.
        fetchButton.IsEnabled = false;
        fetchButton.Content = "Contacting Server";
        weatherText.Text = "";
        hideWeatherImageStoryboard.Begin(this);
        
        // Start fetching the weather forecast asynchronously.
        NoArgDelegate fetcher = new NoArgDelegate(
            this.FetchWeatherFromServer);
    
        fetcher.BeginInvoke(null, null);
    }
    

单击按钮时,会显示时钟绘图并开始对其进行动画处理。 禁用该按钮。 我们调用FetchWeatherFromServer新线程,然后我们方法返回,允许Dispatcher处理事件时我们将等待收集天气预报。

  • 获取天气

    C#
    private void FetchWeatherFromServer()
    {
        // Simulate the delay from network access.
        Thread.Sleep(4000);              
        
        // Tried and true method for weather forecasting - random numbers.
        Random rand = new Random();
        String weather;
    
        if (rand.Next(2) == 0)
        {
            weather = "rainy";
        }
        else
        {
            weather = "sunny";
        }
    
        // Schedule the update function in the UI thread.
        tomorrowsWeather.Dispatcher.BeginInvoke(
            System.Windows.Threading.DispatcherPriority.Normal,
            new OneArgDelegate(UpdateUserInterface), 
            weather);
    }
    

为简便起见,本例中没有任何网络代码。 通过使新线程进入休眠状态四秒钟,模拟网络访问的延迟。 在此时间,原始UI线程时仍运行并对事件作出响应。 为了对此进行演示,我们让动画保持运行状态,最小化和最大化按钮也继续工作。

当完成时延迟,并且我们已随机选择我们天气预报时,这是时间来报告回UI线程。 我们执行此操作通过计划调用UpdateUserInterfaceUI线程使用此线程的Dispatcher。 将描述天气的字符串传递给此计划方法调用。

  • 更新UI

    C#
    private void UpdateUserInterface(String weather)
    {    
        //Set the weather image
        if (weather == "sunny")
        {       
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "SunnyImageSource"];
        }
        else if (weather == "rainy")
        {
            weatherIndicatorImage.Source = (ImageSource)this.Resources[
                "RainingImageSource"];
        }
    
        //Stop clock animation
        showClockFaceStoryboard.Stop(this);
        hideClockFaceStoryboard.Begin(this);
    
        //Update UI text
        fetchButton.IsEnabled = true;
        fetchButton.Content = "Fetch Forecast";
        weatherText.Text = weather;     
    }
    

DispatcherUI线程有时间时,它会执行的计划的调用UpdateUserInterface。 此方法停止时钟动画,并选择一张映像用于描述天气。 它将显示此映像,并还原“获取预报”按钮。

多窗口、多线程

某些WPF应用程序需要多个顶级窗口。 它是完全可以接受一个线程 /Dispatcher组合来管理多个时段,但有时多个线程执行的更好的作业。 尤其当这些窗口中的某一个将有可能要独占线程时,更是如此。

Windows 资源管理器以这种方式工作。 每个新资源管理器窗口都属于原始进程,但它是在独立线程的控件下创建的。

通过使用WPFFrame控件,我们可以显示网页。 我们可以轻松地创建一个简单Internet Explorer替换。 让我们从一个重要功能开始:打开新资源管理器窗口的能力。 当用户单击“新建窗口”按钮时,我们将在单独的线程中启动窗口的副本。 这样一来,在其中一个窗口中的长时间运行或阻塞操作将不会锁定其他窗口。

在实际情况下,Web 浏览器模型自身拥有复杂的线程模型。 由于大多数读者都熟悉它,所以我们选择它。

以下示例显示了代码。

XAML
<Window x:Class="SDKSamples.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MultiBrowse"
    Height="600" 
    Width="800"
    Loaded="OnLoaded"
    >
  <StackPanel Name="Stack" Orientation="Vertical">
    <StackPanel Orientation="Horizontal">
      <Button Content="New Window"
              Click="NewWindowHandler" />
      <TextBox Name="newLocation"
               Width="500" />
      <Button Content="GO!"
              Click="Browse" />
    </StackPanel>

    <Frame Name="placeHolder"
            Width="800"
            Height="550"></Frame>
  </StackPanel>
</Window>
C#
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Threading;
using System.Threading;


namespace SDKSamples
{
    public partial class Window1 : Window
    {

        public Window1() : base()
        {
            InitializeComponent();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
           placeHolder.Source = new Uri("http://www.msn.com");
        }

        private void Browse(object sender, RoutedEventArgs e)
        {
            placeHolder.Source = new Uri(newLocation.Text);
        }

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

        private void ThreadStartingPoint()
        {
            Window1 tempWindow = new Window1();
            tempWindow.Show();       
            System.Windows.Threading.Dispatcher.Run();
        }
    }
}

此代码中的以下线程段对我们来说是最有趣的:

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

当单击“新建窗口”按钮时,将调用该方法。 它创建了一个新线程,并以异步方式启动。

C#
private void ThreadStartingPoint()
{
    Window1 tempWindow = new Window1();
    tempWindow.Show();       
    System.Windows.Threading.Dispatcher.Run();
}

此方法是新线程的起点。 我们在此线程的控件下创建了一个新窗口。 WPF自动创建一个新Dispatcher来管理新线程。 我们所要做,以使该窗口功能是启动Dispatcher

技术详细信息和疑难点

使用线程处理编写组件

Microsoft .NET Framework开发人员指南描述如何组件可以公开向其客户端的异步行为的模式 (请参阅基于事件的异步模式概述)。 例如,假设我们想要打包FetchWeatherFromServer方法划分为可重用、 非图形的组件。 采用标准Microsoft .NET Framework模式,这看起来应如下所示。

C#
public class WeatherComponent : Component
{
    //gets weather: Synchronous 
    public string GetWeather()
    {
        string weather = "";

        //predict the weather

        return weather;
    }

    //get weather: Asynchronous 
    public void GetWeatherAsync()
    {
        //get the weather
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
}

public class GetWeatherCompletedEventArgs : AsyncCompletedEventArgs
{
    public GetWeatherCompletedEventArgs(Exception error, bool canceled,
        object userState, string weather)
        :
        base(error, canceled, userState)
    {
        _weather = weather;
    }

    public string Weather
    {
        get { return _weather; }
    }
    private string _weather;
}

public delegate void GetWeatherCompletedEventHandler(object sender,
    GetWeatherCompletedEventArgs e);

GetWeatherAsync 将使用上述的技术之一(如创建后台线程)来以异步方式工作,而非阻止调用线程。

此模式的最重要的部分之一调用MethodName Completed方法调用在同一线程MethodName Async方法的开头。 你可以使用执行这类情况的操作WPF非常轻松,通过将存储CurrentDispatcher-但然后非图形组件无法只能用在WPF应用程序,不在Windows 窗体或ASP.NET程序。

DispatcherSynchronizationContext类满足这一需要 — 它看作的简化版本Dispatcher适用于其他UI以及框架。

C#
public class WeatherComponent2 : Component
{
    public string GetWeather()
    {
        return fetchWeatherFromServer();
    }

    private DispatcherSynchronizationContext requestingContext = null;

    public void GetWeatherAsync()
    {
        if (requestingContext != null)
            throw new InvalidOperationException("This component can only handle 1 async request at a time");

        requestingContext = (DispatcherSynchronizationContext)DispatcherSynchronizationContext.Current;

        NoArgDelegate fetcher = new NoArgDelegate(this.fetchWeatherFromServer);

        // Launch thread
        fetcher.BeginInvoke(null, null);
    }

    private void RaiseEvent(GetWeatherCompletedEventArgs e)
    {
        if (GetWeatherCompleted != null)
            GetWeatherCompleted(this, e);
    }

    private string fetchWeatherFromServer()
    {
        // do stuff
        string weather = "";

        GetWeatherCompletedEventArgs e =
            new GetWeatherCompletedEventArgs(null, false, null, weather);

        SendOrPostCallback callback = new SendOrPostCallback(DoEvent);
        requestingContext.Post(callback, e);
        requestingContext = null;

        return e.Weather;
    }

    private void DoEvent(object e)
    {
        //do stuff
    }

    public event GetWeatherCompletedEventHandler GetWeatherCompleted;
    public delegate string NoArgDelegate();
}

嵌套泵

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

具有“确定”按钮的消息框

一些线程必须负责消息框窗口。 WPF 可以为消息框窗口创建新线程,但此线程无法在原始窗口中绘制禁用的元素(请回忆之前所讨论的互相排斥)。 相反,WPF使用嵌套的消息处理系统。 Dispatcher类包括一个称为特殊方法PushFrame,它用于存储应用程序的当前执行点然后开始新的消息循环。 嵌套的消息循环完成时,将在原始后恢复执行PushFrame调用。

在这种情况下,PushFrame维护对的调用的程序上下文MessageBoxShow,并开始新的消息循环,以重新绘制背景窗口并处理输入消息框窗口。当用户单击确定,并清除弹出窗口中时,嵌套的循环退出,控件将在调用后恢复Show

过时的路由事件

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

XAML
<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线程。 其中存在问题,因为 COM STA 对象上创建UI线程可以仅释放上UI线程。 CLR的等效BeginInvoke(在这种情况下使用 Win32 的SendMessage)。 但是,如果UI线程处于忙碌状态、 终结器线程将停止,无法释放 COM STA 对象,这种结构造成严重的内存泄漏。 因此CLR团队进行严格的调用来使锁的工作方式一样。

有关任务WPF是为了避免意外的重新进入不重新引入内存泄漏,这正是我们避免阻塞无处不在可重入性。

请参阅

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


原文链接 https://docs.microsoft.com/zh-cn/dotnet/framework/wpf/advanced/threading-model#Y8383

posted @ 2018-03-30 18:20  死鱼眼の猫  阅读(179)  评论(0编辑  收藏  举报