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 ...
随机推荐
- MySQL系列:索引失效场景总结
相关文章 数据库系列:MySQL慢查询分析和性能优化 数据库系列:MySQL索引优化总结(综合版) 数据库系列:高并发下的数据字段变更 数据库系列:覆盖索引和规避回表 数据库系列:数据库高可用及无损扩 ...
- 二次元 & 动漫壁纸网站(内容记录)
前言 天天和电脑.手机以及平板等电子设备打交道,一个好看的桌面壁纸图片当然是必不可少的,也曾经分享过<值得珍藏的高清壁纸网站推荐>,各种类型和分辨率的壁纸都有. 今天再分享些「高清二次元& ...
- 在 .NET 中使用 OPC UA 协议
目录 什么是 OPC UA UaExpert 的使用 下载 UaExpert 首次启动 添加 OPC UA 服务器 连接 OPC UA 服务器 查看 PLC 数据 使用 C# 读写 OPC UA 数据 ...
- 替代 Redis 的开源项目「GitHub 热点速览」
近日,知名开源项目 Redis 宣布修改开源协议,从原来的「BSD 3-Clause 开源协议」改成「RSALv2 和 SSPLv1 双重许可证」.新的许可证主要是限制托管 Redis 产品的云服务商 ...
- 【Spring注解驱动开发】你敢信?面试官竟然让我现场搭建一个AOP测试环境!
写在前面 今天是9月1号,金九银十的跳槽黄金期已拉开序幕,相信很多小伙伴也在摩拳擦掌,想换一个新的工作环境.然而,由于今年疫情的影响,很多企业对于招聘的要求是越来越严格.之前,很多不被问及的知识点,最 ...
- UE4 c++重构简单死亡之眼的效果
虚幻社区中有蓝图教学视频 使用C++重构,主要用到UGameplayStatics类中的SetGlobalTimerDilation方法,以及角色的相机管理器的调用,之后通过StartCameraFa ...
- UE4Gameplay定时器
参考 定时器在全局定时器管理器(FTimerManager类)中管理,对于每个实例Uobject和场景都会有全局定时器管理器,一般来说通过SetTimer和SetTimerForNextTick来设置 ...
- UE4中的GamePlay模块
链接 该文档主要通过学习自己构建文件,形成GamePlay模块.下图是利用引擎创建的一个空模板C++代码结构 简要流程 UBT 虚幻编译工具(UBT:Unreal Build Tool)是一个自定义工 ...
- Windows下获取设备管理器列表信息-setupAPI
背景及问题: 在与硬件打交道时,经常需要知道当前设备连接的硬件信息,以便连接正确的硬件,比如串口通讯查询连接的硬件及端口,一般手工的方式就是去设备管理器查看相应的信息,应用程序如何读取这一部分信息呢, ...
- 国民经济行业分类与代码(GB/T 4754-2002、GB/T 4754-2011、GB/T 4754-2017)并存入MySQL数据库【可获取下载】
戳链接下载:https://download.csdn.net/download/weixin_45556024/34913490 或关注公众号[靠谱杨阅读人生]回复[行业]获取. 整理不易,资源fu ...