当前位置: 首页 > news >正文

将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本)

如果你要在 WPF 程序中使用线程池完成一个特殊的任务,那么使用 .NET 的 API Task.Run 并传入一个 Lambda 表达式可以完成。不过,使用 Lambda 表达式会带来变量捕获的一些问题,比如说你需要区分一个变量作用于是在 Lambda 表达式中,还是当前上下文全局(被 Lambda 表达式捕获到的变量)。然后,在静态分析的时候,也难以知道此 Lambda 表达式在整个方法中的执行先后顺序,不利于分析潜在的 Bug。

在使用 async/await 关键字编写异步代码的时候,虽然说实质上也是捕获变量,但这时没有显式写一个 Lambda 表达式,所有的变量都是被隐式捕获的变量,写起来就像在一个同步方法一样,便于理解。


本文内容

    • C++/WinRT
    • C# / .NET / WPF 版本
    • Raymond Chen 的版本

C++/WinRT

以下 C++/WinRT 的代码来自 Raymond Chen 的示例代码。Raymond Chen 写了一个 UWP 的版本用于模仿 C++/WinRT 的线程切换效果。在看他编写的 UWP 版本之前我也思考了可以如何实现一个 .NET / WPF 的版本,然后成功做出了这样的效果。

Raymond Chen 的版本可以参见:C++/WinRT envy: Bringing thread switching tasks to C# (UWP edition) - The Old New Thing。

winrt::fire_and_forget MyPage::Button_Click()
{
  // We start on a UI thread.
  auto lifetime = get_strong();

  // Get the control's value from the UI thread.
  auto v = SomeControl().Value();

  // Move to a background thread.
  co_await winrt::resume_background();

  // Do the computation on a background thread.
  auto result1 = Compute1(v);
  auto other = co_await ContactWebServiceAsync();
  auto result2 = Compute2(result1, other);

  // Return to the UI thread to provide an interim update.
  co_await winrt::resume_foreground(Dispatcher());

  // Back on the UI thread: We can update UI elements.
  TextBlock1().Text(result1);
  TextBlock2().Text(result2);

  // Back to the background thread to do more computations.
  co_await winrt::resume_background();

  auto extra = co_await GetExtraDataAsync();
  auto result3 = Compute3(result1, result2, extra);

  // Return to the UI thread to provide a final update.
  co_await winrt::resume_foreground(Dispatcher());

  // Update the UI one last time.
  TextBlock3().Text(result3);
}

可以看到,使用 co_await winrt::resume_background(); 可以将线程切换至线程池,使用 co_await winrt::resume_foreground(Dispatcher()); 可以将线程切换至 UI。

也许你会觉得这样没什么好处,因为 C#/.NET 的版本里面 Lambda 表达式一样可以这么做:

await Task.Run(() =>
{
    // 这里的代码会在线程池执行。
});
// 这里的代码会回到 UI 线程执行。

但是,现在我们给出这样的写法:

// 仅在某些特定的情况下才使用线程池执行,而其他情况依然在主线程执行 DoSomething()。
if (condition) {
  co_await winrt::resume_background();
}

DoSomething();

你就会发现 Lambda 的版本变得很不好理解了。

C# / .NET / WPF 版本

我们现在编写一个自己的 Awaiter 来实现这样的线程上下文切换。

关于如何编写一个 Awaiter,可以阅读我的其他博客:

  • 定义一组抽象的 Awaiter 的实现接口,你下次写自己的 await 可等待对象时将更加方便 - 吕毅
  • .NET 中什么样的类是可使用 await 异步等待的? - 吕毅
  • .NET 除了用 Task 之外,如何自己写一个可以 await 的对象? - 吕毅

这里,我直接贴出我编写的 DispatcherSwitcher 类的全部源码。

using System;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using System.Windows.Threading;

namespace Walterlv.ThreadSwitchingTasks
{
    public static class DispatcherSwitcher
    {
        public static ThreadPoolAwaiter ResumeBackground() => new ThreadPoolAwaiter();

        public static ThreadPoolAwaiter ResumeBackground(this Dispatcher dispatcher)
            => new ThreadPoolAwaiter();

        public static DispatcherAwaiter ResumeForeground(this Dispatcher dispatcher) =>
            new DispatcherAwaiter(dispatcher);

        public class ThreadPoolAwaiter : INotifyCompletion
        {
            public void OnCompleted(Action continuation)
            {
                Task.Run(() =>
                {
                    IsCompleted = true;
                    continuation();
                });
            }

            public bool IsCompleted { get; private set; }

            public void GetResult()
            {
            }

            public ThreadPoolAwaiter GetAwaiter() => this;
        }

        public class DispatcherAwaiter : INotifyCompletion
        {
            private readonly Dispatcher _dispatcher;

            public DispatcherAwaiter(Dispatcher dispatcher) => _dispatcher = dispatcher;

            public void OnCompleted(Action continuation)
            {
                _dispatcher.InvokeAsync(() =>
                {
                    IsCompleted = true;
                    continuation();
                });
            }

            public bool IsCompleted { get; private set; }

            public void GetResult()
            {
            }

            public DispatcherAwaiter GetAwaiter() => this;
        }
    }
}

Raymond Chen 取的类名是 ThreadSwitcher,不过我认为可能 Dispatcher 在 WPF 中更能体现其线程切换的含义。

于是,我们来做一个试验。以下代码在 MainWindow.xaml.cs 里面,如果你使用 Visual Studio 创建一个 WPF 的空项目的话是可以找到的。随便放一个 Button 添加事件处理函数。

private async void DemoButton_Click(object sender, RoutedEventArgs e)
{
    var id0 = Thread.CurrentThread.ManagedThreadId;

    await Dispatcher.ResumeBackground();

    var id1 = Thread.CurrentThread.ManagedThreadId;

    await Dispatcher.ResumeForeground();

    var id2 = Thread.CurrentThread.ManagedThreadId;
}

id0 和 id2 在主线程上,id1 是线程池中的一个线程。

这样,我们便可以在一个上下文中进行线程切换了,而不需要使用 Task.Run 通过一个 Lambda 表达式来完成这样的任务。

现在,这种按照某些特定条件才切换到后台线程执行的代码就很容易写出来了。

// 仅在某些特定的情况下才使用线程池执行,而其他情况依然在主线程执行 DoSomething()。
if (condition)
{
    await Dispatcher.ResumeBackground();
}

DoSomething();

Raymond Chen 的版本

Raymond Chen 后来在另一篇博客中也编写了一份 WPF / Windows Forms 的线程切换版本。请点击下方的链接跳转至原文阅读:

  • C++/WinRT envy: Bringing thread switching tasks to C# (WPF and WinForms edition) - The Old New Thing

我在为他的代码添加了所有的注释后,贴在了下面:

using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Windows.Forms;
using System.Windows.Threading;

namespace Walterlv.Windows.Threading
{
    /// <summary>
    /// 提供类似于 WinRT 中的线程切换体验。
    /// </summary>
    /// <remarks>
    /// https://devblogs.microsoft.com/oldnewthing/20190329-00/?p=102373
    /// https://blog.walterlv.com/post/bring-thread-switching-tasks-to-csharp-for-wpf.html
    /// </remarks>
    public class ThreadSwitcher
    {
        /// <summary>
        /// 将当前的异步等待上下文切换到 WPF 的 UI 线程中继续执行。
        /// </summary>
        /// <param name="dispatcher">WPF 一个 UI 线程的调度器。</param>
        /// <returns>一个可等待对象,使用 await 等待此对象可以使后续任务切换到 UI 线程执行。</returns>
        public static DispatcherThreadSwitcher ResumeForegroundAsync(Dispatcher dispatcher) =>
            new DispatcherThreadSwitcher(dispatcher);

        /// <summary>
        /// 将当前的异步等待上下文切换到 Windows Forms 的 UI 线程中继续执行。
        /// </summary>
        /// <param name="control">Windows Forms 的一个控件。</param>
        /// <returns>一个可等待对象,使用 await 等待此对象可以使后续任务切换到 UI 线程执行。</returns>
        public static ControlThreadSwitcher ResumeForegroundAsync(Control control) =>
            new ControlThreadSwitcher(control);

        /// <summary>
        /// 将当前的异步等待上下文切换到线程池中继续执行。
        /// </summary>
        /// <returns>一个可等待对象,使用 await 等待此对象可以使后续的任务切换到线程池执行。</returns>
        public static ThreadPoolThreadSwitcher ResumeBackgroundAsync() =>
            new ThreadPoolThreadSwitcher();
    }

    /// <summary>
    /// 提供一个可切换到 WPF 的 UI 线程执行上下文的可等待对象。
    /// </summary>
    public struct DispatcherThreadSwitcher : INotifyCompletion
    {
        internal DispatcherThreadSwitcher(Dispatcher dispatcher) =>
            _dispatcher = dispatcher;

        /// <summary>
        /// 当使用 await 关键字异步等待此对象时,将调用此方法返回一个可等待对象。
        /// </summary>
        public DispatcherThreadSwitcher GetAwaiter() => this;

        /// <summary>
        /// 获取一个值,该值指示是否已完成线程池到 WPF UI 线程的切换。
        /// </summary>
        public bool IsCompleted => _dispatcher.CheckAccess();

        /// <summary>
        /// 由于进行线程的上下文切换必须使用 await 关键字,所以不支持调用同步的 <see cref="GetResult"/> 方法。
        /// </summary>
        public void GetResult()
        {
        }

        /// <summary>
        /// 当异步状态机中的前一个任务结束后,将调用此方法继续下一个任务。在此可等待对象中,指的是切换到 WPF 的 UI 线程。
        /// </summary>
        /// <param name="continuation">将异步状态机推进到下一个异步状态。</param>
        public void OnCompleted(Action continuation) => _dispatcher.BeginInvoke(continuation);

        private readonly Dispatcher _dispatcher;
    }

    /// <summary>
    /// 提供一个可切换到 Windows Forms 的 UI 线程执行上下文的可等待对象。
    /// </summary>
    public struct ControlThreadSwitcher : INotifyCompletion
    {
        internal ControlThreadSwitcher(Control control) =>
            _control = control;

        /// <summary>
        /// 当使用 await 关键字异步等待此对象时,将调用此方法返回一个可等待对象。
        /// </summary>
        public ControlThreadSwitcher GetAwaiter() => this;

        /// <summary>
        /// 获取一个值,该值指示是否已完成线程池到 Windows Forms UI 线程的切换。
        /// </summary>
        public bool IsCompleted => !_control.InvokeRequired;

        /// <summary>
        /// 由于进行线程的上下文切换必须使用 await 关键字,所以不支持调用同步的 <see cref="GetResult"/> 方法。
        /// </summary>
        public void GetResult()
        {
        }

        /// <summary>
        /// 当异步状态机中的前一个任务结束后,将调用此方法继续下一个任务。在此可等待对象中,指的是切换到 Windows Forms 的 UI 线程。
        /// </summary>
        /// <param name="continuation">将异步状态机推进到下一个异步状态。</param>
        public void OnCompleted(Action continuation) => _control.BeginInvoke(continuation);

        private readonly Control _control;
    }

    /// <summary>
    /// 提供一个可切换到线程池执行上下文的可等待对象。
    /// </summary>
    public struct ThreadPoolThreadSwitcher : INotifyCompletion
    {
        /// <summary>
        /// 当使用 await 关键字异步等待此对象时,将调用此方法返回一个可等待对象。
        /// </summary>
        public ThreadPoolThreadSwitcher GetAwaiter() => this;

        /// <summary>
        /// 获取一个值,该值指示是否已完成 UI 线程到线程池的切换。
        /// </summary>
        public bool IsCompleted => SynchronizationContext.Current == null;

        /// <summary>
        /// 由于进行线程的上下文切换必须使用 await 关键字,所以不支持调用同步的 <see cref="GetResult"/> 方法。
        /// </summary>
        public void GetResult()
        {
        }

        /// <summary>
        /// 当异步状态机中的前一个任务结束后,将调用此方法继续下一个任务。在此可等待对象中,指的是切换到线程池中。
        /// </summary>
        /// <param name="continuation">将异步状态机推进到下一个异步状态。</param>
        public void OnCompleted(Action continuation) => ThreadPool.QueueUserWorkItem(_ => continuation());
    }
}

参考资料

  • C++/WinRT envy: Bringing thread switching tasks to C# (UWP edition) - The Old New Thing
  • C++/WinRT envy: Bringing thread switching tasks to C# (WPF and WinForms edition) - The Old New Thing

我的博客会首发于 https://blog.walterlv.com/,而 CSDN 会从其中精选发布,但是一旦发布了就很少更新。

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

知识共享许可协议

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

相关文章:

  • 如何在 MSBuild 的项目文件 csproj 中获取绝对路径
  • C# 跨设备前后端开发探索
  • MSBuild 如何编写带条件的属性、集合和任务 Condition?
  • WPF 像素着色器入门:使用 Shazzam Shader Editor 编写 HLSL 像素着色器代码
  • 如何快速自定义 Visual Studio 中部分功能的快捷键
  • C# 8.0 如何在项目中开启可空引用类型的支持
  • C# 8.0 可空引用类型中的各项警告/错误的含义和示例代码
  • C# 可空引用类型 NullableReferenceTypes 更强制的约束:将警告改为错误 WarningsAsErrors
  • ClearType 的原理:Windows 上文本的亚像素控制
  • 使用 7-Zip 的命令行版本来压缩和解压文件
  • 在项目文件 csproj 中或者 MSBuild 的 Target 中使用 % 引用集合中每一项的属性
  • MSBuild 中的特殊字符($ @ % 等):含义、用法以及转义
  • WPF 获取元素(Visual)相对于屏幕设备的缩放比例,可用于清晰显示图片
  • Visual Studio 通过修改项目的调试配置文件做到临时调试的时候不要编译(解决大项目编译缓慢问题)
  • 使用 dotnet 命令行配合 vscode 完成一个完整 .NET 解决方案的编写和调试
  • ES6 学习笔记(一)let,const和解构赋值
  • Node.js 新计划:使用 V8 snapshot 将启动速度提升 8 倍
  • tweak 支持第三方库
  • V4L2视频输入框架概述
  • vue:响应原理
  • 番外篇1:在Windows环境下安装JDK
  • 服务器之间,相同帐号,实现免密钥登录
  • 基于OpenResty的Lua Web框架lor0.0.2预览版发布
  • 技术:超级实用的电脑小技巧
  • 开源中国专访:Chameleon原理首发,其它跨多端统一框架都是假的?
  • 漫谈开发设计中的一些“原则”及“设计哲学”
  • 排序算法之--选择排序
  • 学习HTTP相关知识笔记
  • 鱼骨图 - 如何绘制?
  • 运行时添加log4j2的appender
  • #我与Java虚拟机的故事#连载07:我放弃了对JVM的进一步学习
  • #我与虚拟机的故事#连载20:周志明虚拟机第 3 版:到底值不值得买?
  • $.ajax()方法详解
  • $.ajax中的eval及dataType
  • (C语言)求出1,2,5三个数不同个数组合为100的组合个数
  • (附源码)springboot学生选课系统 毕业设计 612555
  • (官网安装) 基于CentOS 7安装MangoDB和MangoDB Shell
  • (收藏)Git和Repo扫盲——如何取得Android源代码
  • (原創) X61用戶,小心你的上蓋!! (NB) (ThinkPad) (X61)
  • ***汇编语言 实验16 编写包含多个功能子程序的中断例程
  • .[backups@airmail.cc].faust勒索病毒的最新威胁:如何恢复您的数据?
  • .L0CK3D来袭:如何保护您的数据免受致命攻击
  • .mat 文件的加载与创建 矩阵变图像? ∈ Matlab 使用笔记
  • .NET Micro Framework初体验
  • .NET 依赖注入和配置系统
  • @JSONField或@JsonProperty注解使用
  • @RequestBody的使用
  • [ C++ ] STL---仿函数与priority_queue
  • [20180312]进程管理其中的SQL Server进程占用内存远远大于SQL server内部统计出来的内存...
  • [ACTF2020 新生赛]Include
  • [ai笔记4] 将AI工具场景化,应用于生活和工作
  • [Ariticle] 厚黑之道 一 小狐狸听故事
  • [BZOJ1060][ZJOI2007]时态同步 树形dp
  • [BZOJ1877][SDOI2009]晨跑[最大流+费用流]
  • [C#]无法获取源 https://api.nuge t.org/v3-index存储签名信息解决方法