代码编织梦想

在 WPF 中,使用 ResourceDictionary 本身不会受到创建线程同步影响,意味着可以在任意的线程创建 ResourceDictionary 资源字典,然后在任意线程使用。但是此时的读写需要有时间上的差距,否则将会多线程读写不安全。在 ResourceDictionary 有一个 CanBeAccessedAcrossThreads 属性用来决定在进行读写的时候是否加上锁,但这个属性是内部的,需要通过黑科技更改才能用上

依据 WPF 的源代码,可以看到 ResourceDictionary 类继承了 IDictionary 接口,也开放了 Add 和 Clear 和 Contains 等方法,在这些方法的实现里面,都会先判断 CanBeAccessedAcrossThreads 属性的值,然后决定是否加上锁进行安全读写

        public void Add(object key, object value)
        {
            // Seal styles and templates within App and Theme dictionary
            SealValue(value);

            if (CanBeAccessedAcrossThreads)
            {
                lock(((ICollection)this).SyncRoot)
                {
                    AddWithoutLock(key, value);
                }
            }
            else
            {
                AddWithoutLock(key, value);
            }

        }

        /// <summary>
        ///     Removes all elements from the IDictionary.
        /// </summary>
        public void Clear()
        {
            if (CanBeAccessedAcrossThreads)
            {
                lock(((ICollection)this).SyncRoot)
                {
                    ClearWithoutLock();
                }
            }
            else
            {
                ClearWithoutLock();
            }
        }

因此想要进行多线程安全的读写就需要设置 CanBeAccessedAcrossThreads 属性,而这个属性的定义如下

        internal bool CanBeAccessedAcrossThreads
        {
            get { return ReadPrivateFlag(PrivateFlags.CanBeAccessedAcrossThreads); }
            set { WritePrivateFlag(PrivateFlags.CanBeAccessedAcrossThreads, value); }
        }

也就是说这是一个内部的属性,只有 FrameworkTemplate 和 Style 两个类才能给他赋值。而 FrameworkTemplate 是一个抽象类,不过 DataTemplate 继承了 FrameworkTemplate 类,也就是可以通过 DataTemplate 来设置 Resources.CanBeAccessedAcrossThreads 的值

下面写一个辅助类,用于给 ResourceDictionary 设置允许线程安全读写

    public static class ResourceDictionaryCanBeAccessedAcrossThreadsHelper
    {
        public static void SetCanBeAccessedAcrossThreads(ResourceDictionary resourceDictionary)
        {
            _ = new InnerFrameworkTemplate
            {
                // 在 InnerFrameworkTemplate 的 Resources 属性里面
                // 将会设置 Resources.CanBeAccessedAcrossThreads = true 的值
                // 也就是让 Resources 的读写获取都加上锁
                Resources = resourceDictionary
            };
        }

        private class InnerFrameworkTemplate : DataTemplate
        {

        }
    }

大概的使用方法如下

                var resourceDictionary = new ResourceDictionary();
                ResourceDictionaryCanBeAccessedAcrossThreadsHelper
                    .SetCanBeAccessedAcrossThreads(resourceDictionary);

此时通过 VS 的自动变量,可以看到 resourceDictionary 变量的 CanBeAccessedAcrossThreads 是 true 值

测试的代码如下

        public MainWindow()
        {
            InitializeComponent();

            Task.Run(() =>
            {
                var resourceDictionary = new ResourceDictionary();
                ResourceDictionaryCanBeAccessedAcrossThreadsHelper
                    .SetCanBeAccessedAcrossThreads(resourceDictionary);
                resourceDictionary.Add("Foo", "Fx");

                Dispatcher.InvokeAsync(() =>
                {
                    Resources.MergedDictionaries.Add(resourceDictionary);

                    var foo = Resources["Foo"];
                });
            });
        }

当然了,上面的代码即使不加上 SetCanBeAccessedAcrossThreads 也是可以使用的,在设置和读取之间有时间差

设置之后就可以进行多线程开始安全写入,而没有设置之前依然是允许一个线程写一个线程读的。如下面的测试代码,在调用 AddAndGetValue_OnClick 方法的时候,用的不是线程安全的,而调用 AddAndGetValueWithCanBeAccessedAcrossThreads_OnClick 方法加上 CanBeAccessedAcrossThreads 线程安全,对这两个进行多线程读写

        private void AddAndGetValue_OnClick(object sender, RoutedEventArgs e)
        {
            const int count = 10000000;

            var resourceDictionary = new ResourceDictionary();

            Task.Run(() =>
            {
                for (int i = 0; i < count / 2; i++)
                {
                    resourceDictionary.Add(i, i);
                }

                // 加入完成
                Debugger.Break();
            });

            Task.Run(() =>
            {
                for (int i = count / 2 + 1; i < count; i++)
                {
                    resourceDictionary.Add(i, i);
                }

                // 加入完成
                Debugger.Break();
            });

            Task.Run(() =>
            {
                for (int i = count - 1; i >= 0; i--)
                {
                    _ = resourceDictionary[i];
                }
            });
        }

        private void AddAndGetValueWithCanBeAccessedAcrossThreads_OnClick(object sender, RoutedEventArgs e)
        {
            const int count = 10000000;

            var resourceDictionary = new ResourceDictionary();
            ResourceDictionaryCanBeAccessedAcrossThreadsHelper
                .SetCanBeAccessedAcrossThreads(resourceDictionary);

            Task.Run(() =>
            {
                for (int i = 0; i < count / 2; i++)
                {
                    resourceDictionary.Add(i, i);
                }

                // 加入完成
                Debugger.Break();
            });

            Task.Run(() =>
            {
                for (int i = count / 2 + 1; i < count; i++)
                {
                    resourceDictionary.Add(i, i);
                }

                // 加入完成
                Debugger.Break();
            });

            Task.Run(() =>
            {
                for (int i = count - 1; i >= 0; i--)
                {
                    _ = resourceDictionary[i];
                }
            });
        }

执行测试可以看到在 AddAndGetValue_OnClick 方法将会有 Task.Run 无法执行完成。而 AddAndGetValueWithCanBeAccessedAcrossThreads_OnClick 方法将会全部执行完成

本文代码放在github欢迎小伙伴访问

在 WPF 里面挖了一个坑,在 Contains 方法里面没有加上锁,因此在 XAML 内的使用还请小心,也许会存在字典出错

        public bool Contains(object key)
        {
        	// 这里缺少了 if (CanBeAccessedAcrossThreads) 这样的代码

            bool result = _baseDictionary.Contains(key);

            if (result)
            {
                KeyRecord keyRecord = _baseDictionary[key] as KeyRecord;
                if (keyRecord != null && _deferredLocationList.Contains(keyRecord))
                {
                    return false;
                }
            }

            //Search for the value in the Merged Dictionaries
            if (_mergedDictionaries != null)
            {
                for (int i = MergedDictionaries.Count - 1; (i > -1) && !result; i--)
                {
                    // Note that MergedDictionaries collection can also contain null values
                    ResourceDictionary mergedDictionary = MergedDictionaries[i];
                    if (mergedDictionary != null)
                    {
                        result = mergedDictionary.Contains(key);
                    }
                }
            }
            return result;
        }

在 XAML 里面使用 StaticResourceExtension 也就是 {StaticResource xx} 的方法获取的时候,将会调用到 Contains 方法。但是在使用的时候还请放心,因为理论上对字典以及 WPF 的 Hashtable 进行一个线程写一个线程读是不会有异常的。本文提供的方法只是为了

通过多线程创建资源字典的方法可以用来提升启动性能

当前整个 WPF 源代码都是开源的,请看 https://github.com/dotnet/wpf/

我搭建了自己的博客 https://blog.lindexi.com/ 欢迎大家访问,里面有很多新的博客。只有在我看到博客写成熟之后才会放在csdn或博客园,但是一旦发布了就不再更新

如果在博客看到有任何不懂的,欢迎交流,我搭建了 dotnet 职业技术学院 欢迎大家加入

如有不方便在博客评论的问题,可以加我 QQ 2844808902 交流

知识共享许可协议
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。欢迎转载、使用、重新发布,但务必保留文章署名林德熙(包含链接:http://blog.csdn.net/lindexi_gd ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请与我联系

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 本文链接: https://blog.csdn.net/lindexi_gd/article/details/110531850

WPF 底层 从手指触摸屏幕到笔迹在屏幕显示中间的步骤-爱代码爱编程

整个 WPF 就是一个UI框架,一个 UI 框架最重要的是 交互 和 显示 部分,而书写这个功能将会完全贯穿 WPF 整个框架的功能。本文非入门级博客,本文包含了大量链接博客,阅读本文你将会了解从用户手指触摸屏幕到最终屏幕打印出笔迹的应用程序执行的步骤 本文实际内容不多,但是如果加上链接的博客,那么总内容将会非常多,还请小伙伴仔细阅读本文链接的博客 从

读书笔记 dotnet 什么时候进行垃圾回收-爱代码爱编程

是否有小伙伴好奇如果没有在代码调用垃圾回收,那么框架会在什么时候调用垃圾回收。本文是读还没出版的伟民哥翻译的 .NET内存管理宝典 - 提高代码质量、性能和可扩展性 这本书的笔记 当前是 2020年9月 本文的知识最新就是当前的时间,因为 dotnet 的更新速度十分快,当前由 dotnet 基金会维护整套 dotnet 开源项目。从编译器到运行时全部都

dotnet OpenXML 文本字体的选择规则-爱代码爱编程

在 Office 的文本排版里面,会根据字符选择使用哪个字体插槽。也就是实际上在 Office 里面可以在一个文本段里面指定多个字体,会根据实际的字符使用不同的字体 在做 Office 解析的时候,在 OpenXML SDK 里面是没有找到表示字体的属性的,只能找到 LatinFont 和 EastAsianFont 和 ComplexScriptFon

WPF dotnet core 如何开启 Pointer 消息的支持-爱代码爱编程

在 WPF 下,可以使用和 UWP 一样的 Pointer 触摸架构,只是开启的方式和 .NET Framework 版本有细微的差异 看过 win10 支持默认把触摸提升 Pointer 消息 的小伙伴可以了解到,这个博客的方法是通过配置文件的方式 而在 .NET Core 的 WPF 下是不会去读取 App.config 文件,那么此时应该如何开启

Xamarin 从零开始部署 iOS 上的 Walterlv.CloudKeyboard 应用-爱代码爱编程

本文将告诉大家如何从零开始在 iOS 上部署 Walterlv.CloudKeyboard 应用。这个 Walterlv.CloudKeyboard 应用是一个云输入法应用,在 GitHub 完全开源,采用 Xamarin 开发,用途是让手机接收电脑端的打字输入的输入法。因为我没有在 iOS 上找到任何一款稍微能用的输入法,因此只能拜托太子帮我开发一款应用

推荐官方开源 PInvoke 库 包含大量 win32 封装-爱代码爱编程

在调用 win32 库的时候,小伙伴会遇到的问题是不知道对应的 win32 函数应该如何写。或者在网上抄了的代码的实现都有些诡异,想要自己封装发现工作量太大。好消息是官方将 PInvoke 库在 dotnet 基金会完全开源,包含了大量的 Win32 库,如 gdi32.dll 和 kernel32.dll 和 user32.dll 等 使用官方的库的优

WPF 底层 从手指触摸屏幕到笔迹在屏幕显示中间的步骤-爱代码爱编程

整个 WPF 就是一个UI框架,一个 UI 框架最重要的是 交互 和 显示 部分,而书写这个功能将会完全贯穿 WPF 整个框架的功能。本文非入门级博客,本文包含了大量链接博客,阅读本文你将会了解从用户手指触摸屏幕到最终屏幕打印出笔迹的应用程序执行的步骤 本文实际内容不多,但是如果加上链接的博客,那么总内容将会非常多,还请小伙伴仔细阅读本文链接的博客 从

WPF 在后台代码定义 ResourceDictionary 资源字典-爱代码爱编程

在 WPF 中的 ResourceDictionary 资源字典大部分都是在 XAML 里面定义的,但是在 C# 代码定义一个资源字典也是可行的,只是写起来有点诡异 在 CSharp 后台代码里面给 WPF 定义资源字典需要重新创建一个类,让这个类继承 ResourceDictionary 如以下代码 public class Foo :

制作的 dotnet tool 运行失败提示依赖缺失-爱代码爱编程

小伙伴做了一个很好用的 dotnet tool 工具,但是这个工具仅在他的设备上能运行,在我的设备上运行就会退出提示 An assembly specified in the application dependencies manifest (LindexiDoubi.deps.json) was not found 找不到依赖 默认选择 dotnet

WPF dotnet core 如何开启 Pointer 消息的支持-爱代码爱编程

在 WPF 下,可以使用和 UWP 一样的 Pointer 触摸架构,只是开启的方式和 .NET Framework 版本有细微的差异 看过 win10 支持默认把触摸提升 Pointer 消息 的小伙伴可以了解到,这个博客的方法是通过配置文件的方式 而在 .NET Core 的 WPF 下是不会去读取 App.config 文件,那么此时应该如何开启

推荐官方开源 PInvoke 库 包含大量 win32 封装-爱代码爱编程

在调用 win32 库的时候,小伙伴会遇到的问题是不知道对应的 win32 函数应该如何写。或者在网上抄了的代码的实现都有些诡异,想要自己封装发现工作量太大。好消息是官方将 PInvoke 库在 dotnet 基金会完全开源,包含了大量的 Win32 库,如 gdi32.dll 和 kernel32.dll 和 user32.dll 等 使用官方的库的优

WPF_深入理解画刷-爱代码爱编程

画刷用于填充区域,不管是元素的背景色、前景色以及边框,还是形状的内部填充和笔画(stroke)。 画刷支持更改通知,因为它们继承自Freezable类。如果改变了画刷,任何使用画刷的元素都会自动更新绘制自身。 画刷支持部分透明。为此,只需要修改Opacity属性,使背景能够透过前面的内容进行显示。 通过SystemBrushes类可以访问这样的画刷