WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互等
在应用启动过程里,除了主 UI 线程之外,如果还多启动了新的 UI 线程,且此新的 UI 线程碰到 ContentPresenter 类型,那么将可能存在让新的 UI 线程和主 UI 线程互等。这是多线程安全问题,不是很好复现,即使采用 demo 的代码,也需要几千次运行才能在某些配置比较差的机器上遇到新的 UI 线程和主 UI 线程互等,应用启动失败。本文来告诉大家复现的步骤,以及原因,和解决方法
复现步骤
只需要在主 UI 线程里,加载的资源里面包含 ContentPresenter 类型的初始化。然后在主 UI 线程执行 App 时,同时启动另一个 UI 线程,让另一个 UI 线程碰到 ContentPresenter 类型。碰到 ContentPresenter 类型,让 ContentPresenter 类型的静态构造函数能被执行,代码如下
先在 App.xaml 定义资源,定义的资源刚好碰到 ContentPresenter 类型
<Application x:Class="BerehenachearbairGarciwereyer.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:BerehenachearbairGarciwereyer"
StartupUri="MainWindow.xaml">
<Application.Resources>
<Style x:Key="DefaultButtonStyle" TargetType="Button">
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Width" Value="100" />
<Setter Property="Height" Value="100" />
<Setter Property="Margin" Value="10,10,10,10" />
<Setter Property="Template" >
<Setter.Value>
<ControlTemplate>
<ContentPresenter Content="123"></ContentPresenter>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Application.Resources>
</Application>
大家都知道,在 WPF 里的 XAML 将会被构建为 BAML 文件,在启动过程里面加载 BAML 将需要调用到 WPF 底层,将 BAML 展开内存。如上代码将需要创建 ContentPresenter 对象
在 App.xaml.cs 里,在 App 构造函数再启动另一个 UI 线程,在新 UI 线程里面访问 ContentPresenter 类型的 ContentProperty 属性,这是一个静态属性,在类型在程序集第一次碰到,将会调用类型的静态构造函数
public partial class App : Application
{
public App()
{
RunNewUIThread();
}
public static void RunNewUIThread()
{
Thread thread = new(Run);
thread.SetApartmentState(ApartmentState.STA);
thread.Start();
void Run()
{
var currentDispatcher =
System.Windows.Threading.Dispatcher.CurrentDispatcher;
currentDispatcher.InvokeAsync(() =>
{
TouchContentPresenter();
});
System.Windows.Threading.Dispatcher.Run();
}
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static void TouchContentPresenter()
{
// Just call the .cctor in ContentPresenter.
var property = ContentPresenter.ContentProperty;
CaptureObject(property);
}
private static void CaptureObject(object obj)
{
Debug.WriteLine(obj);
}
}
以上的代码为了不在 Release 下被优化,于是写了 TouchContentPresenter 和 CaptureObject 两个方法。类型的静态构造函数是在类型被碰到之前,放在 TouchContentPresenter 方法里面,可以让代码在准备调用 TouchContentPresenter 方法时才尝试执行 ContentPresenter 的静态构造函数。同时加上 MethodImplOptions.NoInlining 让代码不会被内联
再加上 CaptureObject 方法,强行捕获参数,从而让获取属性的代码不会被优化
复现的代码放在 https://github.com/lindexi/lindexi_gd/tree/de8bdfbf4715c7200631913cecd24749c98228a3/BerehenachearbairGarciwereyer 上,拉下来之后,构建运行,大概运行几千次,预计是可以复现
在复现时,可以看到线程 Id 为 22436 的主 UI 线程在等待 ContentPresenter 的静态构造函数完成,如下图

这是因为在 .NET 里面,一个类型的静态构造函数,只能由一个线程执行,不会存在多线程同时执行静态构造函数。如果有某个线程在执行静态构造函数,那么其他的线程将需要等待静态构造函数执行完成才能继续碰类型。也就是相当于静态构造函数进入时加了锁,需要在执行完成之后才会释放锁,其他的线程都在等待静态构造函数的锁,也就是等待静态构造函数执行完
在线程 Id 为 16100 的新 UI 线程,执行到 ContentPresenter 的静态构造函数,然而静态构造函数在等待一个被主 UI 线程拿到的锁,静态构造函数无法执行完成

原理
核心原因是一个不良设计导致的,在 ContentPresenter 的静态构造函数里面,干的活太多了。其中就包括调用了 CreateTextBlockFactory 等方法,如下代码
static ContentPresenter()
{
DataTemplate template;
FrameworkElementFactory text;
Binding binding;
// Default template for strings when hosted in ContentPresener with RecognizesAccessKey=true
template = new DataTemplate();
text = CreateAccessTextFactory();
text.SetValue(AccessText.TextProperty, new TemplateBindingExtension(ContentProperty));
template.VisualTree = text;
template.Seal();
s_AccessTextTemplate = template;
// Default template for strings
template = new DataTemplate();
text = CreateTextBlockFactory();
text.SetValue(TextBlock.TextProperty, new TemplateBindingExtension(ContentProperty));
template.VisualTree = text;
template.Seal();
s_StringTemplate = template;
// 忽略其他代码
}
internal static FrameworkElementFactory CreateAccessTextFactory()
{
FrameworkElementFactory text = new FrameworkElementFactory(typeof(AccessText));
return text;
}
在 CreateAccessTextFactory 创建的 FrameworkElementFactory 对象的构造函数代码如下,在构造函数将会给 FrameworkElementFactory.Type 属性赋值
public FrameworkElementFactory(Type type, string name)
{
Type = type;
Name = name;
}
然而 FrameworkElementFactory.Type 属性是比较复杂的,在赋值积分啊里面将会调用到 XamlReader.BamlSharedSchemaContext.GetKnownXamlType 方法
public Type Type
{
get { return _type; }
set
{
// 忽略其他代码
// If this is a KnownType in the BamlSchemaContext, then there is a faster way to create
// an instance of that type than using Activator.CreateInstance. So in that case
// save the delegate for later creation.
WpfKnownType knownType = null;
if (_type != null)
{
knownType = XamlReader.BamlSharedSchemaContext.GetKnownXamlType(_type) as WpfKnownType;
}
_knownTypeFactory = (knownType != null) ? knownType.DefaultConstructor : null;
}
}
在 GetKnownXamlType 里面将需要等待 _syncObject 对象的锁。然而 XamlReader.BamlSharedSchemaContext 是一个静态属性,这就意味着在使用此属性,无论是主 UI 线程还是新 UI 线程都拿到相同的 WpfSharedBamlSchemaContext 类型对象,也就是说调用到 WpfSharedBamlSchemaContext 的其他方法时,等待的是相同的一个 _syncObject 对象
internal XamlType GetKnownXamlType(Type type)
{
XamlType xamlType;
lock (_syncObject)
{
// 忽略其他代码
}
return xamlType;
}
如果是在 新 UI 线程先碰到 ContentPresenter 类型,那么 ContentPresenter 的静态构造函数将在 新 UI 线程执行。执行的静态构造函数将会等待 WpfSharedBamlSchemaContext 的 _syncObject 对象的锁。如果刚好主 UI 线程正在展开 Baml 需要使用 Create_BamlProperty_ContentPresenter_ContentSource 方法,那么在此方法进入时,将因为碰到了 ContentPresenter 类型,需要等待 ContentPresenter 的静态构造函数执行完成
[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.NoInlining)]
private WpfKnownMember Create_BamlProperty_ContentPresenter_ContentSource()
{
Type type = typeof(System.Windows.Controls.ContentPresenter);
DependencyProperty dp = System.Windows.Controls.ContentPresenter.ContentSourceProperty;
var bamlMember = new WpfKnownMember( this, // Schema Context
this.GetXamlType(typeof(System.Windows.Controls.ContentPresenter)), // DeclaringType
"ContentSource", // Name
dp, // DependencyProperty
false, // IsReadOnly
false // IsAttachable
);
bamlMember.TypeConverterType = typeof(System.ComponentModel.StringConverter);
bamlMember.Freeze();
return bamlMember;
}
在进入 Create_BamlProperty_ContentPresenter_ContentSource 方法之前,其实主 UI 线程已获取了 _syncObject 对象的锁。也就是说 ContentPresenter 的静态构造函数必须等待主 UI 线程释放锁才能完成,然而主 UI 线程必须等待 ContentPresenter 的静态构造函数执行完成才能释放锁
于是就构成了两个线程相互等待。在主 UI 线程进入 Create_BamlProperty_ContentPresenter_ContentSource 方法,需要等待 ContentPresenter 的静态构造函数执行完成,才能释放主 UI 线程的锁,让 ContentPresenter 的静态构造继续执行。执行在新 UI 线程的 ContentPresenter 的静态构造函数在等待主 UI 线程释放锁才能执行完成。主 UI 线程在等待新 UI 线程的静态构造函数执行完成。新 UI 线程在等待主 UI 线程等待静态构造函数执行完成之后释放的锁
两个 UI 线程进入摸鱼,应用就起不来
看到以上的原理,在实际的应用里面,想要遇到这个坑还是很难。因为 ContentPresenter 的静态构造函数只会执行一次,谁能说一定不在主 UI 线程执行?而且即使在新 UI 线程执行,那也不一定刚好在进入静态构造函数,主 UI 线程也需要用到 ContentPresenter 的相关属性。这个是需要刚好的,如果在主 UI 线程需要用到 ContentPresenter 的相关属性比较前,就在新 UI 线程进入 ContentPresenter 的静态构造函数,那将因为在新 UI 线程能等到锁而成功执行完成 ContentPresenter 的静态构造函数。如果在主 UI 线程碰到 ContentPresenter 的相关属性时,那么此时的 ContentPresenter 的静态构造函数就由主 UI 线程执行,也没有任何问题。只有在主 UI 线程拿到了锁,在准备碰到 ContentPresenter 的上一个方法时,也就是 WpfSharedBamlSchemaContext.CreateKnownMember 方法,此时的主 UI 线程已拿到锁,在新 UI 线程进入 ContentPresenter 的静态构造函数,如此才能让两个线程相互等待
解决方法
了解了原理,解决方法就十分简单了,只需要不让 ContentPresenter 的静态构造方法被新的 UI 线程调度执行即可。在新的 UI 线程执行之前,先碰一下 ContentPresenter 类型即可,例如获取此类型的某个属性之类,如以下代码
[MethodImpl(MethodImplOptions.NoInlining)]
private static void TouchContentPresenter()
{
// Just call the .cctor in ContentPresenter.
var property = ContentPresenter.ContentProperty;
CaptureObject(property);
}
private static void CaptureObject(object obj)
{
Debug.WriteLine(obj);
}
在开启新的 UI 线程之前,先调用一下 TouchContentPresenter 方法即可。由于碰到了类型里面的某个属性,无论是否静态,都会先调用对应的类型的静态构造函数,静态构造函数只会被调用一次,因此即可解决线程安全问题
另一个解决方法是不要尝试在应用启动的过程里面开启多个 UI 线程。在应用启动完成之后,再开启,就基本不会遇到此问题
这个问题已报告给 WPF 官方,详细请看 Multi UI thread visit the ContentPresenter at application startup may deadlock · Issue #6609 · dotnet/wpf
我认为这也是一个设计缺陷,稍微熟悉 .NET 的开发者都知道,在静态构造函数里面碰锁是很危险的。因为静态构造函数的调用是不确定的,取决于第一次碰到此类型的代码进入之前。因此静态构造函数里面的碰锁的时机将是不可预期的。再加上静态构造函数只能被调用一次,这就让其他多线程碰到此类型,都需要等待静态构造函数执行完成。由于静态构造函数的调用是不可预期的,多线程里只有一个线程能进入静态构造函数,其他线程需要等待,于是此等待就相当于一个锁,如果在静态构造函数里面会碰到另一个锁,那就相当于有两个锁。有两个锁加上不可预期的调用,那这个逻辑很好构成相互等待
WPF 应用启动过程同时启动多个 UI 线程且访问 ContentPresenter 可能让多个 UI 线程互等的更多相关文章
- WPF [调用线程无法访问此对象,因为另一个线程拥有该对象。] 解决方案以及如何实现字体颜色的渐变
本文说明WPF [调用线程无法访问此对象,因为另一个线程拥有该对象.] 解决方案以及如何实现字体颜色的渐变 先来看看C#中Timer的简单说明,你想必猜到实现需要用到Timer的相关知识了吧. C# ...
- WPF解决方案------调用线程无法访问此对象,因为另一个线程拥有该对象
WPF [调用线程无法访问此对象,因为另一个线程拥有该对象.] 解决方案 在这里以播放图片为例进行说明,代码如下: void _Timer_Elapsed(object sender, Elapsed ...
- Linux系统在启动过程中启动级别发生错误的解决办法
一.系统启动级别一共有六个: 0:系统停机模式,系统不可以正常启动 1:单用户模式, root权限,用于系统的维护,禁止远程登陆 2:多用户模式,没有NFS网络支持 3:完整的多用户文本模式,有NFS ...
- 调用线程无法访问此对象,因为另一个线程拥有该对象 [c# wpf定时器程序报的错误]
WPF:Dispatcher.Invoke 方法,只有在其上创建 Dispatcher 的线程才可以直接访问DispatcherObject.若要从不同于在其上创建 DispatcherObject ...
- WPF 出现“调用线程无法访问此对象,因为另一个线程拥有该对象”
引起这种错误多半是由于在非UI线程刷新界面,解决此问题可以使用Dispatcher this.Dispatcher.Invoke(new Action(() => { UpdateUI(stri ...
- [原] KVM 虚拟化原理探究(2)— QEMU启动过程
KVM 虚拟化原理探究- QEMU启动过程 标签(空格分隔): KVM [TOC] 虚拟机启动过程 第一步,获取到kvm句柄 kvmfd = open("/dev/kvm", O_ ...
- Android源码——Activity组件的启动过程
根Activity启动过程 Launcher启动MainActivity的过程主要分为6个步骤: 一.Launcher向ActivityManagerService发送一个启动MainActivity ...
- Spark 启动过程(standalone)
Spark启动过程 正常启动Spark集群时往往使用start-all.sh ,此脚本中通过调用start-master.sh和start-slaves.sh启动mater及workers节点. 1. ...
- HDFS Namenode启动过程
文章作者:luxianghao 文章来源:http://www.cnblogs.com/luxianghao/p/6564032.html 转载请注明,谢谢合作. 免责声明:文章内容仅代表个人观点, ...
- Linux 启动过程详解
目录 1. Linux启动过程 2. 启动过程概述 3. 引导加载阶段 4. 内核阶段 4.1 内核加载阶段 4.2 内核启动阶段 5. 早期的用户空间 6. 初始化过程 6.1 SysV init ...
随机推荐
- win7笔记本、台式机装centos7过程记录
1.国内镜像网站下载centos的iso文件 链接点我: 2.找个u盘,格式化为NTFS格式,这样才能传4G以上大小的文件 3.iso直接复制到u盘是不行的,必须做启动盘.下载个ultra做,官网地 ...
- AMD、request.js,生词太多,傻傻搞不清
前言 之前在公司用JS写前端页面,本来自己是一个写后端的,但是奈何人少,只能自己也去写了.但是自己对前端基本不懂,基本就是照着前人写的照着抄,反正大体意思是明白的,但是出现问题了,基本上也是吭哧吭哧好 ...
- CentOS 7.6 防火墙打开、关闭,端口开启、关闭
查看CentOS版本 cat /etc/redhat-release 显示系统名.节点名称.操作系统的发行版号.操作系统版本.运行系统的机器 ID 号. uname -a 防火墙命令 #查询防火墙状态 ...
- vue三种插槽
1. 作用:让父组件可以向子组件指定位置插入html结构,也是一种组件间通信的方式,适用于 父组件 ===> 子组件 . 2. 分类:默认插槽.具名插槽.作用域插槽 3. 使用方式: a.默认插 ...
- #dp,矩阵乘法#洛谷 5371 [SNOI2019]纸牌
题目 一副纸牌有 \(n\) 种,每种有 \(m\) 张, 现在有 \(k\) 个限制条件形如第 \(k_i\) 种牌至少选 \(a_i\) 张, 一个三元组合法当且仅当其为 \((i,i+1,i+2 ...
- OpenHarmony应用全局的UI状态存储:AppStorage
AppStorage是应用全局的UI状态存储,是和应用的进程绑定的,由UI框架在应用程序启动时创建,为应用程序UI状态属性提供中央存储. 和AppStorage不同的是,LocalStorage是 ...
- C++ 异常和错误处理机制:如何使您的程序更加稳定和可靠
在C++编程中,异常处理和错误处理机制是非常重要的.它们可以帮助程序员有效地处理运行时错误和异常情况.本文将介绍C++中的异常处理和错误处理机制. 什么是异常处理? 异常处理是指在程序执行过程中发生异 ...
- 信息泄露漏洞的JS整改方案
引言 ️ 日常工作中,我们经常会面临线上环境被第三方安全厂商扫描出JS信息泄露漏洞的情况,这给我们的系统安全带来了潜在威胁.但幸运的是,对于这类漏洞的整改并不复杂.本文将介绍几种可行的整改方法,以及其 ...
- HarmonyOS 极客马拉松2023 正式启动,诚邀极客们用键盘码出无限可能!
原文:https://mp.weixin.qq.com/s/p2yIs0rMmDE2BwhzsAtr7A,点击链接查看更多技术内容. 2023年6月15日, HarmonyOS极客马拉松2023开 ...
- k8s之helm部署mysql集群
一.简介 Helm Helm 是 Kubernetes 的包管理器. Chart Helm使用的包格式称为 chart.chart存储在Chart Repository. chart就是一个描述Kub ...