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

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

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


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);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

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

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

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

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

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

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

C# / .NET / WPF 版本

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

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

这里,我直接贴出我编写的 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;
}
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62

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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

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

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

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

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

Raymond Chen 的版本

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

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

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());
}
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 127
  • 128
  • 129
  • 130
  • 131
  • 132
  • 133
  • 134
  • 135
  • 136
  • 137
  • 138

参考资料


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

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

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

发布了382 篇原创文章 · 获赞 232 · 访问量 47万+

将 C++/WinRT 中的线程切换体验带到 C# 中来(WPF 版本)的更多相关文章

  1. 用代码说话:如何在Java中实现线程

    并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...

  2. java中线程切换的开销

    思路: 开三个线程A,B,C 线程A不断的调用LockSupport.park()阻塞自己,一旦发现自己被唤醒,调用Thread.interrupted()清除interrupt标记位,同时增加自增计 ...

  3. C#中的线程(一)入门

    文章系参考转载,英文原文网址请参考:http://www.albahari.com/threading/ 作者 Joseph Albahari,  翻译 Swanky Wu 中文翻译作者把原文放在了& ...

  4. C#中的线程二(Cotrol.BeginInvoke和Control.Invoke)

    C#中的线程二(Cotrol.BeginInvoke和Control.Invoke) 原文地址:http://www.cnblogs.com/whssunboy/archive/2007/06/07/ ...

  5. Java中的线程

    http://hi.baidu.com/ochzqvztdbabcir/item/ab9758f9cfab6a5ac9f337d4 相濡以沫 Java语法总结 - 线程 一 提到线程好像是件很麻烦很复 ...

  6. Windows API学习---用户方式中的线程同步

    前言 当所有的线程在互相之间不需要进行通信的情况下就能够顺利地运行时, Micrsoft Windows的运行性能最好.但是,线程很少能够在所有的时间都独立地进行操作.通常情况下,要生成一些线程来处理 ...

  7. C++11 中的线程、锁和条件变量

    转自:http://blog.jobbole.com/44409/ 线程 类std::thread代表一个可执行线程,使用时必须包含头文件<thread>.std::thread可以和普通 ...

  8. Android中UI线程与后台线程交互设计的5种方法

    我想关于这个话题已经有很多前辈讨论过了.今天算是一次学习总结吧. 在android的设计思想中,为了确保用户顺滑的操作体验.一 些耗时的任务不能够在UI线程中运行,像访问网络就属于这类任务.因此我们必 ...

  9. C#中的线程(上)-入门 分类: C# 线程 2015-03-09 10:56 53人阅读 评论(0) 收藏

    1.     概述与概念 C#支持通过多线程并行地执行代码,一个线程有它独立的执行路径,能够与其它的线程同时地运行.一个C#程序开始于一个单线程,这个单线程是被CLR和操作系统(也称为"主线 ...

随机推荐

  1. mysql数据库数据入库时间跟当前时间差了8个小时

    vim /etc/my.cnf[mysqld]default-time_zone = '+8:00'重启mysql服务./etc/init.d/mysqld restart 未测试

  2. [Gamma]Scrum Meeting#3

    github 本次会议项目由PM召开,时间为5月28日晚上10点30分 时长10分钟 任务表格 人员 昨日工作 下一步工作 木鬼 撰写博客,组织例会 撰写博客,组织例会 swoip 前端显示屏幕,翻译 ...

  3. 利用Windows内置工具winsat测试硬盘速度(SSD&机械盘对比)

    利用Windows内置工具winsat测试硬盘速度(SSD&机械盘对比) 以下是红色内容是在命令行运行: C:\Users\Administrator>winsat diskWindow ...

  4. leetcode 688. “马”在棋盘上的概率

    题目描述: 已知一个 NxN 的国际象棋棋盘,棋盘的行号和列号都是从 0 开始.即最左上角的格子记为 (0, 0),最右下角的记为 (N-1, N-1). 现有一个 “马”(也译作 “骑士”)位于 ( ...

  5. js中[object Object]与object.prototype.toString.call()

    最近在用node读取文件中的json数据后,用JSON.parse()转成了json,然后响应数据传给前端,发现输出值object对象时显示[object object],在这里我们来看一下他的具体意 ...

  6. 【Eclipse】Eclipse如何导出java项目为jar包

    1.首先确定要导出的项目 从项目结构可以看出,笔者的项目是一个Dynamic Java Project.com/db下面有一个config的数据库配置文件.WEB-INF/lib文件夹下面有依赖的ja ...

  7. python MySQLdb 字典(dict)结构数据插入mysql

    背景: 有时候直接操作数据库字段比较多,一个个写比较麻烦,而且如果字段名跟数据库一致,那生成为字典后,是否能直接使用字典写入数据库呢,这样会方便很多,这里简单介绍一种方法. 实例: 1. 假设数据库表 ...

  8. Celery 服务搭建

    整个项目工程如下 __init__.py """ 注意点:python3.7 需要执行 pip install --upgrade https://github.com/ ...

  9. Consider defining a bean of type 'com.*.*.mapper.*.*Mapper' in your configuration.

    @Mapper 不能加载的问题 Consider defining a bean of type 'com.*.*.mapper.*.*Mapper' in your configuration. 添 ...

  10. mysql删除唯一索引

    在项目中用spring data jpa指定了一个唯一索引: @Entity @Table(name = "t_product") @Getter @Setter @AllArgs ...