├── 3.task ├── 10.task_mana.md ├── images │ ├── 3801.png │ ├── Delay.png │ ├── Then.png │ ├── il_1.png │ ├── Parallel.png │ ├── Schedule.png │ ├── 1690100215198.png │ ├── image-1588063128308.png │ ├── image-1588063628600.png │ ├── image-1588071312510.png │ ├── image-1588255310584.png │ ├── image-1588255339414.png │ ├── image-1588255373842.png │ ├── image-1588492862423.png │ ├── image-1588497534212.png │ ├── image-1588498203820.png │ ├── image-20220731165128240.png │ ├── image-20230723152844855.png │ └── 1315495-20201203234026782-1609280077.png ├── 7.redis.md ├── README.md ├── 8.async_state.md ├── 9.task_scheduler.md ├── 5.async_await.md ├── 1.task1.md ├── 3.task3.md └── 2.task2.md ├── Dockerfile ├── 1.thread_basic ├── README.md ├── images │ ├── 线程创建流程.png │ ├── image-1587130109424.png │ ├── image-1587202311528.png │ ├── image-20220326094054220.png │ ├── image-20220326094118503.png │ ├── image-20220327111619615.png │ ├── image-20220327120102986.png │ ├── image-20220327120630357.png │ ├── image-20220327121434655.png │ ├── image-20220327122655002.png │ ├── image-20220327132231528.png │ ├── image-20220327144415497.png │ ├── image-20220327152524746.png │ ├── image-20220327153359457.png │ ├── image-20220327172520496.png │ ├── image-20220327173212888.png │ ├── image-20220327175223992.png │ ├── image-20220327211237162.png │ ├── image-20220327211531022.png │ ├── image-20220327211555818.png │ ├── image-20220327213745505.png │ ├── image-20220329063238458.png │ ├── image-20220329070927419.png │ ├── image-20220329072017948.png │ ├── image-20220329072022154.png │ ├── image-20220329073441047.png │ ├── image-20220730195247672.png │ ├── image-20220730195846906.png │ ├── image-20220730200444017.png │ ├── image-20220730201012499.png │ ├── image-20220730224443903.png │ ├── image-20220730224602236.png │ └── image-20220730230305100.png └── 2.thread_model.md ├── 2.thread_sync ├── images │ ├── 并行协调.gif │ ├── 线程通知.gif │ ├── 进程同步.gif │ ├── Mutex1.gif │ ├── Mutex2.gif │ ├── 自动线程通知.gif │ ├── 1587217550(1).png │ ├── Semaphoregif.gif │ ├── image-1586660083905.png │ ├── image-1586681324216.png │ ├── image-1586684447732.png │ ├── image-1587130109424.png │ ├── image-1587174060064.png │ ├── image-1587217831610.png │ ├── image-1587256361021.png │ ├── image-1587773756667.png │ ├── image-1587871331160.png │ ├── image-1588255310584.png │ ├── image-20220326092016946.png │ ├── image-20220327132544351.png │ ├── image-20220327152524746.png │ ├── image-20220731102225174.png │ ├── image-20220731104739339.png │ ├── image-20220731104741291.png │ ├── image-20220731105229745.png │ └── image-20220731150700668.png ├── README.md ├── 6.manualresetevent.md ├── 7.countdownevent.md ├── 8.barrier.md ├── 5.autorestevent.md ├── 2.locker_monitor.md ├── 4.semaphore.md ├── 10.spinwait.md ├── 3.mutex.md ├── 9.reader_writer_lock.md └── 1.interlocked.md ├── tj.js ├── .gitignore ├── SUMMARY.md ├── README.md ├── undefined.md └── book.json /3.task/10.task_mana.md: -------------------------------------------------------------------------------- 1 | # 3.10 后台任务管理 -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM nginx:latest 3 | COPY _book /usr/share/nginx/html -------------------------------------------------------------------------------- /1.thread_basic/README.md: -------------------------------------------------------------------------------- 1 | # 1. 线程基础 2 | 3 | 在本章中,将会介绍线程的基础使用方法、线程模型、和线程池,这些都是了解 C# 多线程的基础。 4 | -------------------------------------------------------------------------------- /3.task/images/3801.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/3801.png -------------------------------------------------------------------------------- /3.task/images/Delay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/Delay.png -------------------------------------------------------------------------------- /3.task/images/Then.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/Then.png -------------------------------------------------------------------------------- /3.task/images/il_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/il_1.png -------------------------------------------------------------------------------- /3.task/images/Parallel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/Parallel.png -------------------------------------------------------------------------------- /3.task/images/Schedule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/Schedule.png -------------------------------------------------------------------------------- /2.thread_sync/images/并行协调.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/并行协调.gif -------------------------------------------------------------------------------- /2.thread_sync/images/线程通知.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/线程通知.gif -------------------------------------------------------------------------------- /2.thread_sync/images/进程同步.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/进程同步.gif -------------------------------------------------------------------------------- /1.thread_basic/images/线程创建流程.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/线程创建流程.png -------------------------------------------------------------------------------- /2.thread_sync/images/Mutex1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/Mutex1.gif -------------------------------------------------------------------------------- /2.thread_sync/images/Mutex2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/Mutex2.gif -------------------------------------------------------------------------------- /2.thread_sync/images/自动线程通知.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/自动线程通知.gif -------------------------------------------------------------------------------- /3.task/7.redis.md: -------------------------------------------------------------------------------- 1 | # 3.7 使用 TaskCompletionSource 异步状态机编写 Redis 客户端 2 | 3 | https://www.cnblogs.com/whuanle/p/13956549.html -------------------------------------------------------------------------------- /3.task/images/1690100215198.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/1690100215198.png -------------------------------------------------------------------------------- /2.thread_sync/images/1587217550(1).png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/1587217550(1).png -------------------------------------------------------------------------------- /2.thread_sync/images/Semaphoregif.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/Semaphoregif.gif -------------------------------------------------------------------------------- /3.task/images/image-1588063128308.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588063128308.png -------------------------------------------------------------------------------- /3.task/images/image-1588063628600.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588063628600.png -------------------------------------------------------------------------------- /3.task/images/image-1588071312510.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588071312510.png -------------------------------------------------------------------------------- /3.task/images/image-1588255310584.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588255310584.png -------------------------------------------------------------------------------- /3.task/images/image-1588255339414.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588255339414.png -------------------------------------------------------------------------------- /3.task/images/image-1588255373842.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588255373842.png -------------------------------------------------------------------------------- /3.task/images/image-1588492862423.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588492862423.png -------------------------------------------------------------------------------- /3.task/images/image-1588497534212.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588497534212.png -------------------------------------------------------------------------------- /3.task/images/image-1588498203820.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-1588498203820.png -------------------------------------------------------------------------------- /3.task/images/image-20220731165128240.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-20220731165128240.png -------------------------------------------------------------------------------- /3.task/images/image-20230723152844855.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/image-20230723152844855.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-1587130109424.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-1587130109424.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-1587202311528.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-1587202311528.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1586660083905.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1586660083905.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1586681324216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1586681324216.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1586684447732.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1586684447732.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587130109424.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587130109424.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587174060064.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587174060064.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587217831610.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587217831610.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587256361021.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587256361021.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587773756667.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587773756667.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1587871331160.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1587871331160.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-1588255310584.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-1588255310584.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220326092016946.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220326092016946.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220327132544351.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220327132544351.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220327152524746.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220327152524746.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220731102225174.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220731102225174.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220731104739339.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220731104739339.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220731104741291.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220731104741291.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220731105229745.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220731105229745.png -------------------------------------------------------------------------------- /2.thread_sync/images/image-20220731150700668.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/2.thread_sync/images/image-20220731150700668.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220326094054220.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220326094054220.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220326094118503.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220326094118503.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327111619615.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327111619615.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327120102986.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327120102986.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327120630357.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327120630357.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327121434655.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327121434655.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327122655002.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327122655002.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327132231528.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327132231528.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327144415497.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327144415497.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327152524746.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327152524746.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327153359457.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327153359457.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327172520496.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327172520496.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327173212888.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327173212888.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327175223992.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327175223992.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327211237162.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327211237162.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327211531022.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327211531022.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327211555818.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327211555818.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220327213745505.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220327213745505.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220329063238458.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220329063238458.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220329070927419.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220329070927419.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220329072017948.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220329072017948.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220329072022154.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220329072022154.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220329073441047.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220329073441047.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730195247672.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730195247672.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730195846906.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730195846906.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730200444017.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730200444017.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730201012499.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730201012499.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730224443903.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730224443903.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730224602236.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730224602236.png -------------------------------------------------------------------------------- /1.thread_basic/images/image-20220730230305100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/1.thread_basic/images/image-20220730230305100.png -------------------------------------------------------------------------------- /3.task/images/1315495-20201203234026782-1609280077.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/whuanle/csharp_thread/HEAD/3.task/images/1315495-20201203234026782-1609280077.png -------------------------------------------------------------------------------- /tj.js: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Node rules: 3 | ## Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 4 | .grunt 5 | 6 | ## Dependency directory 7 | ## Commenting this out is preferred by some people, see 8 | ## https://docs.npmjs.com/misc/faq#should-i-check-my-node_modules-folder-into-git 9 | node_modules 10 | 11 | # Book build output 12 | _book 13 | 14 | # eBook build output 15 | *.epub 16 | *.mobi 17 | *.pdf 18 | *.vs 19 | _book -------------------------------------------------------------------------------- /2.thread_sync/README.md: -------------------------------------------------------------------------------- 1 | # 2. 线程同步 2 | 3 | 同步(synchronization)是指协调并发操作,得到可以预测的结果的行为。同步在多个线程访问相同的数据时显得尤为重要,但这种操作很容易出现问题。最简单实用的同步工具是后面章节介绍的延续(continuation)和任务组合器。延续和任务组合器将并发程序构造为异步操作,减少了对锁和信号发送的依赖。但即便如此,很多时候仍然需要依赖那些同步底层结构。 4 | 5 | 同步结构可以分为三类:排他锁排他锁每一次只允许一个线程执行特定的活动或一段代码。它的主要目的是令线程访问共享的写状态而不互相影响。排他锁包括lock、Mutex和SpinLock。非排他锁非排他锁实现了有限的并发性。 6 | 7 | 非排他锁包括Semaphore(Slim)和 ReaderWriterLock(Slim)。信号发送结构这种结构允许线程在接到一个或者多个其他线程的通知之前保持阻塞状态。信号发送结构包括ManualResetEvent(Slim)、AutoResetEvent、CountdownEvent和Barrier。前三者就是所谓的事件等待句柄(event waithandle)。 8 | 9 | 一些结构在不使用锁的前提下也可以(巧妙地)处理特定的共享状态的同步操作,称为非阻塞同步结构(nonblocking synchronization construct)。它们包括Thread.MemoryBarrier、Thread.VolatileRead、Thread.VolatileWrite、volatile关键字和Interlocked类。 10 | -------------------------------------------------------------------------------- /3.task/README.md: -------------------------------------------------------------------------------- 1 | # 3. 异步任务 2 | 3 | 4 | 5 | 笔者写这个系列的文章,参考了 《C# 7.0 核心技术指南》、《C# 7.0 本质论》、《C# 多线程编程实战(原书第二版)》、微软文档和 Google 的资料。 6 | 7 | 《C# 7.0 核心技术指南》、《C# 7.0 本质论》这两本书,对多线程,异步这些方面,对于已经掌握的开发者来说,可以补充知识点,对于初学者就不太友好了,学习路线很曲折,不利于初学者学习。笔者觉得技术指南比本质论好一些。 8 | 9 | C# 多线程编程实战(原书第二版)》这边书就没必要看了。。。因为这本书是基于 .NET Fx 4 的,有不少写法是过时的了。另外这边书几乎没用说到原理解析方面的,主要是示例多。示例内容对中文读者也不友好,而且大多数为了示例而示例,对于应用场景方面的使用很缺。 10 | 11 | 微软文档的话,主要是参考 API 和解释,但是中文翻译一言难尽。文档中的示例,假如说你在学习一个读写的锁,但是里面出现了很多 Task 等的代码一起组成示例。这就很迷,但是这个是文档,不是教程。也不利于系统式学习。 12 | 13 | 上面的资料和书籍,具体好不好,适合不适合,怎么学,就要看个人怎么选择啦。 14 | 15 | 第三部分的规划如下: 16 | 17 | * 第一、二、三篇讲解 Task 的使用方法,在这三篇中主要关注 Task 的各种 API 使用方法,不会涉及异步。 18 | 19 | * 第四篇讲解如何使用 Task 编排工作调度,设置并发、执行顺序、延续等。 20 | 21 | * 第五篇讲解 async、await 的使用方法,以及异步使用场景。 22 | 23 | * 第六篇讲解 Task、ValueTask 的区别和 ValueTask 的使用场景。 24 | 25 | * 第七篇讲解如何自己实现一个异步状态机。 26 | 27 | * 第八篇讲解如何实现 TaskScheduler ,自定义 Task 管理。 28 | 29 | * 第九篇讲解如何使用 Task 编写消费者程序,进行任务管理。 30 | -------------------------------------------------------------------------------- /SUMMARY.md: -------------------------------------------------------------------------------- 1 | # C# 多线程与异步 2 | 3 | * [文档导读](README.md) 4 | * [1. 线程基础](1.thread_basic/README.md) 5 | [1.1 Thread 基础](1.thread_basic/1.thread.md) 6 | [1.2 多线程模型](1.thread_basic/2.thread_model.md) 7 | [1.3 线程池](1.thread_basic/3.pool.md) 8 | * [2. 线程同步 - 锁](2.thread_sync/README.md) 9 | [2.1 原子操作 Interlocked](2.thread_sync/1.interlocked.md) 10 | [2.2 Locker 和 Monitor 排他锁](2.thread_sync/2.locker_monitor.md) 11 | [2.3 进程互斥锁 Mutex(排他锁) ](2.thread_sync/3.mutex.md) 12 | [2.4 非排他锁 Semaphore](2.thread_sync/4.semaphore.md) 13 | [2.5 自动线程通知 AutoRestEvent](2.thread_sync/5.autorestevent.md) 14 | [2.6 手动线程通知 ManualResetEvent](2.thread_sync/6.manualresetevent.md) 15 | [2.7 线程完成数 CountdownEvent ](2.thread_sync/7.countdownevent.md) 16 | [2.8 并行协调 Barrier ](2.thread_sync/8.barrier.md) 17 | [2.9 读写锁 ReaderWriterLock](2.thread_sync/9.reader_writer_lock.md) 18 | [2.10 自旋 SpinWait](2.thread_sync/10.spinwait.md) 19 | * [3. 异步任务](3.task/README.md) 20 | [3.1 任务基础 1](3.task/1.task1.md) 21 | [3.2 任务基础 2](3.task/2.task2.md) 22 | [3.3 任务基础 3](3.task/3.task3.md) 23 | [3.4 ValueTask](3.task/4.value_task.md) 24 | [3.5 使用 Task 实现一个任务流](3.task/5.workflow.md) 25 | [3.6 async 和 awiat](3.task/6.async_await.md) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 文档说明 2 | 3 | 作者:痴者工良 4 | 5 | 地址:[https://threads.whuanle.cn](https://threads.whuanle.cn) 6 | 7 | ## 导读 8 | 9 | 此系列教程包括了多线程、锁、同步异步、线程池、任务、async/await、并行、并发等知识点,从零基础掌握多线程和异步,带你了解和走进同步和异步的世界。 10 | 11 | - 教程中每个小节都有代码示例 12 | - 深入原理,讲解深层知识 13 | - 由易到难,从入门到掌握 14 | - 循序渐进,一步步学习,一步步拓展知识面 15 | - 内容完整、齐全,可以系统式学习 16 | - 大量代码示例和场景实践 17 | 18 | 19 | 20 | ### 目录 21 | 22 | * [1. 线程基础](1.thread_basic/README.md) 23 | * [1.1 Thread 基础](1.thread_basic/1.thread.md) 24 | * [1.2 多线程模型](1.thread_basic/2.thread_model.md) 25 | * [1.3 线程池](1.thread_basic/3.pool.md) 26 | * [2. 线程同步](2.thread_sync/README.md) 27 | * [2.1 原子操作 Interlocked](2.thread_sync/1.interlocked.md) 28 | * [2.2 Locker 和 Monitor 排他锁](2.thread_sync/2.locker_monitor.md) 29 | * [2.3 进程互斥锁 Mutex(排他锁) ](2.thread_sync/3.mutex.md) 30 | * [2.4 非排他锁 Semaphore](2.thread_sync/4.semaphore.md) 31 | * [2.5 自动线程通知 AutoRestEvent](2.thread_sync/5.autorestevent.md) 32 | * [2.6 手动线程通知 ManualResetEvent](2.thread_sync/6.manualresetevent.md) 33 | * [2.7 线程完成数 CountdownEvent ](2.thread_sync/7.countdownevent.md) 34 | * [2.8 并行协调 Barrier ](2.thread_sync/8.barrier.md) 35 | * [2.9 读写锁 ReaderWriterLock](2.thread_sync/9.reader_writer_lock.md) 36 | * [2.10 自旋 SpinWait](2.thread_sync/10.spinwait.md) 37 | * [3. Task](3.task/README.md) 38 | * [3.1 任务基础 1](3.task/1.task1.md) 39 | * [3.2 任务基础 2](3.task/2.task2.md) 40 | * [3.3 任务基础 3](3.task/3.task3.md) 41 | * [3.4 ValueTask](3.task/4.value_task.md) 42 | * [3.5 使用 Task 实现一个任务流](3.task/5.workflow.md) 43 | * [3.6 async 和 awiat](3.task/6.async_await.md) -------------------------------------------------------------------------------- /undefined.md: -------------------------------------------------------------------------------- 1 | # 文档说明 2 | 3 | 作者:痴者工良 4 | 5 | 地址:[https://threads.whuanle.cn](https://threads.whuanle.cn) 6 | 7 | ## 导读 8 | 9 | 此系列教程包括了多线程、锁、同步异步、线程池、任务、async/await、并行、并发等知识点,从零基础掌握多线程和异步,带你了解和走进同步和异步的世界。 10 | 11 | - 教程中每个小节都有代码示例 12 | - 深入原理,讲解深层知识 13 | - 由易到难,从入门到掌握 14 | - 循序渐进,一步步学习,一步步拓展知识面 15 | - 内容完整、齐全,可以系统式学习 16 | - 大量代码示例和场景实践 17 | 18 | 19 | 20 | ### 目录 21 | 22 | * [1. 线程基础](1.thread_basic/README.md) 23 | * [1.1 Thread 基础](1.thread_basic/1.thread.md) 24 | * [1.2 多线程模型](1.thread_basic/2.thread_model.md) 25 | * [1.3 线程池](1.thread_basic/3.pool.md) 26 | * [2. 线程同步](2.thread_sync/README.md) 27 | * [2.1 原子操作 Interlocked](2.thread_sync/1.interlocked.md) 28 | * [2.2 Locker 和 Monitor 排他锁](2.thread_sync/2.locker_monitor.md) 29 | * [2.3 进程互斥锁 Mutex(排他锁) ](2.thread_sync/3.mutex.md) 30 | * [2.4 非排他锁 Semaphore](2.thread_sync/4.semaphore.md) 31 | * [2.5 自动线程通知 AutoRestEvent](2.thread_sync/5.autorestevent.md) 32 | * [2.6 手动线程通知 ManualResetEvent](2.thread_sync/6.manualresetevent.md) 33 | * [2.7 线程完成数 CountdownEvent ](2.thread_sync/7.countdownevent.md) 34 | * [2.8 并行协调 Barrier ](2.thread_sync/8.barrier.md) 35 | * [2.9 读写锁 ReaderWriterLock](2.thread_sync/9.reader_writer_lock.md) 36 | * [2.10 自旋 SpinWait](2.thread_sync/10.spinwait.md) 37 | * [3. Task](3.task/README.md) 38 | * [3.1 任务基础 1](3.task/1.task1.md) 39 | * [3.2 任务基础 2](3.task/2.task2.md) 40 | * [3.3 任务基础 3](3.task/3.task3.md) 41 | * [3.4 ValueTask](3.task/4.value_task.md) 42 | * [3.5 使用 Task 实现一个任务流](3.task/5.workflow.md) 43 | * [3.6 async 和 awiat](3.task/6.async_await.md) -------------------------------------------------------------------------------- /book.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | "tbfed-pagefooter", 4 | "anchor-navigation-ex", 5 | "mermaid-gb3", 6 | "insert-logo", 7 | "chapter-fold", 8 | "advanced-emoji", 9 | "github", 10 | "splitter", 11 | "alerts", 12 | "popup", 13 | "prism", 14 | "hide-element", 15 | "head-append", 16 | "-highlight", 17 | "-livereload" 18 | ], 19 | "title": "C# 多线程与异步 - 痴者工良", 20 | "author": "痴者工良", 21 | "description": "这是一本关于 C# 多线程与异步 的书,作者 痴者工良", 22 | "language": "zh-hans", 23 | "links": { 24 | "sidebar": { 25 | "痴者工良的博客": "https://www.whuanle.cn" 26 | } 27 | }, 28 | "pluginsConfig": { 29 | "tbfed-pagefooter": { 30 | "copyright": "Copyright © 痴者工良 2022", 31 | "modify_label": "文档最后更新时间:", 32 | "modify_format": "YYYY-MM-DD HH:mm:ss" 33 | }, 34 | "insert-logo": { 35 | "url": "/images/logo.jpg", 36 | "style": "background: none; max-height: 50px; min-height: 50px" 37 | }, 38 | "github": { 39 | "url": "https://github.com/whuanle/csharp_thread" 40 | }, 41 | "prism": { 42 | "lang": { 43 | "flow": "typescript", 44 | "shell": "bash" 45 | }, 46 | "ignore": [ 47 | "mermaid", 48 | "eval-js" 49 | ], 50 | "css": [ 51 | "prismjs/themes/prism.css" 52 | ], 53 | "js": [ 54 | "prismjs/prism.js", 55 | "prismjs/components.js", 56 | "prismjs/components/prism-csharp.min.js", 57 | "prismjs/components/prism-go.min.js", 58 | "prismjs/components/prism-yaml.min.js", 59 | "prismjs/components/prism-bash.min.js", 60 | "prismjs/components/prism-shell-session.min.js" 61 | ] 62 | }, 63 | "anchor-navigation-ex": { 64 | "showLevel": false, 65 | "showGoTop": false 66 | }, 67 | "hide-element": { 68 | "elements": [ 69 | ".gitbook-link" 70 | ] 71 | }, 72 | "head-append": { 73 | "code": [ 74 | "", 75 | "var _hmt = _hmt || [];", 76 | "(function() {", 77 | " var hm = document.createElement(\"script\");", 78 | " hm.src = \"https://hm.baidu.com/hm.js?976e6171aaac31dae64f4a39e8fe197d\";", 79 | " var s = document.getElementsByTagName(\"script\")[0]; ", 80 | " s.parentNode.insertBefore(hm, s);", 81 | "})();", 82 | "" 83 | ] 84 | } 85 | } 86 | } -------------------------------------------------------------------------------- /2.thread_sync/6.manualresetevent.md: -------------------------------------------------------------------------------- 1 | # 2.6 手动线程通知 2 | 3 | ### 区别与示例 4 | 5 | AutoResetEvent 和 ManualResetEvent 十分相似。两者之间的区别,在于前者是自动(Auto),后者是手动(Manua)。 6 | 7 | 你可以先运行下面的示例,再测试两者的区别。 8 | 9 | 10 | 11 | AutoResetEvent 示例: 12 | 13 | ```csharp 14 | class Program 15 | { 16 | // 线程通知 17 | private static AutoResetEvent resetEvent = new AutoResetEvent(false); 18 | 19 | static void Main(string[] args) 20 | { 21 | // 创建线程 22 | new Thread(DoOne).Start(); 23 | 24 | // 用于不断向另一个线程发送信号 25 | while (true) 26 | { 27 | Console.ReadKey(); 28 | resetEvent.Set(); // 发生通知,设置终止状态 29 | } 30 | } 31 | 32 | public static void DoOne() 33 | { 34 | Console.WriteLine("① 等待中,请发出信号允许我运行"); 35 | resetEvent.WaitOne(); 36 | 37 | Console.WriteLine("② 等待中,请发出信号允许我运行"); 38 | 39 | resetEvent.WaitOne(); 40 | Console.WriteLine("③ 等待中,请发出信号允许我运行"); 41 | 42 | // ... 43 | 44 | Console.WriteLine("线程结束"); 45 | } 46 | } 47 | ``` 48 | 49 | 50 | 51 | ManualResetEvent 类示例: 52 | 53 | ```csharp 54 | class Program 55 | { 56 | private static ManualResetEvent resetEvent = new ManualResetEvent(false); 57 | static void Main(string[] args) 58 | { 59 | new Thread(DoOne).Start(); 60 | // 用于不断向另一个线程发送信号 61 | while (true) 62 | { 63 | Console.ReadKey(); 64 | resetEvent.Set(); // 发生通知,设置终止状态 65 | } 66 | } 67 | 68 | public static void DoOne() 69 | { 70 | Console.WriteLine("等待中,请发出信号允许我运行"); 71 | resetEvent.WaitOne(); 72 | 73 | // 后面的都无效,线程会直接跳过而无需等待 74 | resetEvent.WaitOne(); 75 | resetEvent.WaitOne(); 76 | resetEvent.WaitOne(); 77 | resetEvent.WaitOne(); 78 | resetEvent.WaitOne(); 79 | Console.WriteLine("线程结束"); 80 | } 81 | } 82 | ``` 83 | 84 | 85 | 86 | 因为 AutoResetEvent 对象在 `.WaitOne()` 方法等待信号完毕后,会自动重置为非终止状态,相当于高速收费站自动闸门,一辆车过去后,机器自动关闸。 87 | 88 | ManualResetEvent 相当于人工闸门,打开后编写人工关闭闸门,不然的话闸门会一直处于打开状态。 89 | 90 | ManualResetEvent 主要用于更加灵活的线程信号传递场景。 91 | 92 | 93 | 94 | ## ManualResetEvent 类 95 | 96 | 表示线程同步事件,收到信号时,要想下一次依然生效,必须手动重置该事件。 97 | 98 | 因为 ManualResetEvent 类跟 AutoManualResetEvent 类十分接近,这里就不赘述了。 99 | 100 | 它们的使用区别主要是: 101 | 102 | AutoResetEvent 类,每次 `Set()` ,跳过一个 `WaitOne()`。因为会 `自动恢复设置`,所以下次碰到 `WaitOne()` 会继续等待。 103 | 104 | ManualResetEvent 类, `Set()` 后,不会`重置设置`,因此一旦使用了 `Set()` 后,就会一路放通,不会再等待。 105 | 106 | 107 | 108 | 其构造函数如下: 109 | 110 | | 构造函数 | 说明 | 111 | | ------------------------- | ------------------------------------------------------------ | 112 | | ManualResetEvent(Boolean) | 用一个指示是否将初始状态设置为终止的布尔值初始化 ManualResetEvent 类的新实例。 | 113 | | | | 114 | 115 | 其常用方法如下: 116 | 117 | | 方法 | 说明 | 118 | | -------------------------- | ------------------------------------------------------------ | 119 | | Close() | 释放由当前 WaitHandle 占用的所有资源。 | 120 | | Reset() | 将事件状态设置为非终止,从而导致线程受阻。 | 121 | | Set() | 将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。 | 122 | | WaitOne() | 阻止当前线程,直到当前 WaitHandle 收到信号。 | 123 | | WaitOne(Int32) | 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 | 124 | | WaitOne(Int32, Boolean) | 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 | 125 | | WaitOne(TimeSpan) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。 | 126 | | WaitOne(TimeSpan, Boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。 | 127 | 128 | 129 | 130 | 131 | 132 | ## ManualResetEventSlim 133 | 134 | ManualResetEventSlim 相对 ManualResetEvent ,**当等待时间预计非常短并且事件不跨越进程边界时**,可以使用此类来获得比 ManualResetEvent 更好的性能。 135 | 136 | 从代码使用来看,没有啥区别,主要就是考虑性能下时,两者不同场景。 137 | 138 | 139 | 140 | 这里就不对这两个类型赘述了。 -------------------------------------------------------------------------------- /2.thread_sync/7.countdownevent.md: -------------------------------------------------------------------------------- 1 | # 2.7 线程完成数 2 | 3 | ### 解决一个问题 4 | 5 | 假如,程序需要向一个 Web 发送 5 次请求,受网路波动影响,有一定几率请求失败。如果失败了,就需要重试。 6 | 7 | 示例代码如下: 8 | 9 | ```csharp 10 | class Program 11 | { 12 | private static int count = 0; 13 | static void Main(string[] args) 14 | { 15 | for (int i = 0; i < 5; i++) 16 | new Thread(HttpRequest).Start(); // 创建线程 17 | 18 | // 用于不断向另一个线程发送信号 19 | while (count < 5) 20 | { 21 | Thread.Sleep(100); 22 | } 23 | Console.WriteLine("任务执行完毕"); 24 | } 25 | 26 | 27 | // 模拟网络请求 28 | public static void HttpRequest() 29 | { 30 | Console.WriteLine("开始一个任务"); 31 | // 随机生成一个数,如果为偶数,则模拟请求失败 32 | bool isSuccess = (new Random().Next(0, 10)) % 2 == 0; 33 | 34 | // ... ...模拟请求 HTTP 35 | Thread.Sleep(TimeSpan.FromSeconds(2)); 36 | 37 | // 请求失败则重试 38 | if (!isSuccess) 39 | { 40 | Console.WriteLine($"请求失败,count={count}"); 41 | new Thread(() => 42 | { 43 | HttpRequest(); 44 | }).Start(); 45 | return; 46 | } 47 | // 完成一次任务,+1 48 | Interlocked.Add(ref count,1); 49 | Console.WriteLine($"完成任务,count={count}"); 50 | } 51 | } 52 | ``` 53 | 54 | 这个代码太糟糕了,我们可以使用 CountdownEvent 类来改造它。 55 | 56 | 57 | 58 | ## CountdownEvent 类 59 | 60 | 表示在计数变为零时处于有信号状态的同步基元。 61 | 62 | 也就是说,设定一个计数器,每个线程完成后,就会减去 1 ,当计数器为 0 时,代表所有线程都已经完成了任务。 63 | 64 | 65 | 66 | ### 构造函数和方法 67 | 68 | CountdownEvent 类的构造函数如下: 69 | 70 | | 构造函数 | 说明 | 71 | | --------------------- | ---------------------------------------------- | 72 | | CountdownEvent(Int32) | 使用指定计数初始化 CountdownEvent 类的新实例。 | 73 | 74 | CountdownEvent 类的常用方法如下: 75 | 76 | | 方法 | 说明 | 77 | | --------------------------------- | ------------------------------------------------------------ | 78 | | AddCount() | 将 CountdownEvent 的当前计数加 1。 | 79 | | AddCount(Int32) | 将 CountdownEvent 的当前计数增加指定值。 | 80 | | Reset() | 将 CurrentCount 重置为 InitialCount 的值。 | 81 | | Reset(Int32) | 将 InitialCount 属性重新设置为指定值。 | 82 | | Signal() | 向 CountdownEvent 注册信号,同时减小 CurrentCount 的值。 | 83 | | Signal(Int32) | 向 CountdownEvent 注册多个信号,同时将 CurrentCount 的值减少指定数量。 | 84 | | TryAddCount() | 增加一个 CurrentCount 的尝试。 | 85 | | TryAddCount(Int32) | 增加指定值的 CurrentCount 的尝试。 | 86 | | Wait() | 阻止当前线程,直到设置了 CountdownEvent 为止。 | 87 | | Wait(CancellationToken) | 阻止当前线程,直到设置了 CountdownEvent 为止,同时观察 CancellationToken。 | 88 | | Wait(Int32) | 阻止当前线程,直到设置了 CountdownEvent 为止,同时使用 32 位带符号整数测量超时。 | 89 | | Wait(Int32, CancellationToken) | 阻止当前线程,直到设置了 CountdownEvent 为止,并使用 32 位带符号整数测量超时,同时观察 CancellationToken。 | 90 | | Wait(TimeSpan) | 阻止当前线程,直到设置了 CountdownEvent 为止,同时使用 TimeSpan 测量超时。 | 91 | | Wait(TimeSpan, CancellationToken) | 阻止当前线程,直到设置了 CountdownEvent 为止,并使用 TimeSpan 测量超时,同时观察 CancellationToken。 | 92 | 93 | API 比较多,没事,我们来慢慢了解它。 94 | 95 | 96 | 97 | ### 示例 98 | 99 | 我们来编写一个场景代码,一个有五件事,需要完成,分别派出 5 个人去实现。 100 | 101 | `.Wait();` 用在一个线程中,这个线程将等待其它完成都完成任务后,才能继续往下执行。 102 | 103 | `Signal();` 用于工作线程中,向 CountdownEvent 对象发送信号,告知线程已经完成任务,然后 `CountdownEvent.CurrentCount` 将减去 1。 104 | 105 | 当计数器为 0 时,阻塞的线程将恢复执行。 106 | 107 | 代码示例如下: 108 | 109 | ```csharp 110 | class Program 111 | { 112 | // 手头上有 5 件事 113 | private static CountdownEvent countd = new CountdownEvent(5); 114 | static void Main(string[] args) 115 | { 116 | Console.WriteLine("开始交待任务"); 117 | // 同时叫 5 个人,去做 5 件事 118 | for (int i = 0; i < 5; i++) 119 | { 120 | Thread thread = new Thread(DoOne); 121 | thread.Name = $"{i}"; 122 | thread.Start(); 123 | } 124 | 125 | 126 | // 等他们都完成事情 127 | countd.Wait(); 128 | 129 | Thread.Sleep(500); 130 | Console.WriteLine("任务完成,线程退出"); 131 | Console.ReadKey(); 132 | } 133 | 134 | public static void DoOne() 135 | { 136 | int n = new Random().Next(0, 10); 137 | // 模拟要 n 秒才能完成 138 | Thread.Sleep(TimeSpan.FromSeconds(n)); 139 | // 完成了,减去一件事 140 | countd.Signal(); 141 | Console.WriteLine($" {Thread.CurrentThread.Name}完成一件事了"); 142 | } 143 | } 144 | ``` 145 | 146 | ![image-20220731150700668](images/image-20220731150700668.png) 147 | 148 | 示例很简单,每个线程在完成自己的任务时,需要调用 `Signal()` 方法,使得计数器减去1。 149 | 150 | `.Wait();` 可以等待所有的任务完成。 151 | 152 | 需要注意的是,如果不调用 `Signal()` 或者计数器一直不为0,那么 `Wait()` 将无限等待。 153 | 154 | 当然,`Wait()` 可以设置等待时间, 155 | 156 | 157 | 158 | 另外我们也看到了常用方法中有 `AddCount()`、`Reset()`等。 159 | 160 | 这个类的等待控制方式比较宽松,`Wait()` 后,到底什么时候才能执行,全凭其它线程自觉。 161 | 162 | 如果发现线程执行任务失败,我们可以不调用 `Signal()` 或者 使用 `AddCount()` 来增加次数,进行重试 -------------------------------------------------------------------------------- /2.thread_sync/8.barrier.md: -------------------------------------------------------------------------------- 1 | # 2.8 并行协调 2 | 3 | ### 导读 4 | 5 | 这一篇,我们将学习用于实现并行任务、使得多个线程有序同步完成多个阶段的任务。 6 | 7 | 应用场景主要是控制 N 个线程(可随时增加或减少执行的线程),使得多线程在能够在 M 个阶段中保持同步。 8 | 9 | 线程工作情况如下: 10 | 11 | ![file](./images/image-1587773756667.png) 12 | 13 | 我们接下来 将学习C# 中的 Barrier ,用于实现并行协同工作。 14 | 15 | 16 | 17 | ## Barrier 类 18 | 19 | 使多个任务能够采用并行方式依据某种算法在多个阶段中协同工作,使多个线程(称为“参与者” )分阶段同时处理**算法**。 20 | 21 | 可以使多个线程(称为“参与者” )分阶段同时处理**算法**。(注意算法这个词) 22 | 23 | 每个参与者完成阶段任务后后将被阻止继续执行,直至所有参与者都已达到同一阶段。 24 | 25 | 26 | 27 | Barrier 的构造函数如下: 28 | 29 | | 构造函数 | 说明 | 30 | | ---------------------- | --------------------------- | 31 | | Barrier(Int32) | 初始化 Barrier 类的新实例。 | 32 | | Barrier(Int32, Action) | 初始化 Barrier 类的新实例。 | 33 | 其中一个构造函数定义如下: 34 | ```csharp 35 | public Barrier (int participantCount, Action postPhaseAction); 36 | ``` 37 | 38 | participantCount :处于的线程数量,大于0并且小于32767。 39 | 40 | postPhaseAction :在每个阶段后执行 Action(委托)。 41 | 42 | 43 | 44 | ### 属性和方法 45 | 46 | 在还没有清楚这个类有什么作用前,我们来看一下这个类的常用属性和方法。 47 | 48 | 大概了解 Barrier 有哪些常用属性和方法后,我们开始编写示例代码。 49 | 50 | 属性: 51 | 52 | | 属性 | 说明 | 53 | | --------------------- | ------------------------------------------------ | 54 | | CurrentPhaseNumber | 获取屏障的当前阶段的编号。 | 55 | | ParticipantCount | 获取屏障中参与者的总数。 | 56 | | ParticipantsRemaining | 获取屏障中尚未在当前阶段发出信号的参与者的数量。 | 57 | 58 | 方法: 59 | 60 | | 方法 | 说明 | 61 | | ------------------------------------------ | ------------------------------------------------------------ | 62 | | AddParticipant() | 通知 Barrier,告知其将会有另一个参与者。 | 63 | | AddParticipants(Int32) | 通知 Barrier,告知其将会有多个其他参与者。 | 64 | | RemoveParticipant() | 通知 Barrier,告知其将会减少一个参与者。 | 65 | | RemoveParticipants(Int32) | 通知 Barrier,告知其将会减少一些参与者。 | 66 | | SignalAndWait() | 发出参与者已达到屏障并等待所有其他参与者也达到屏障。 | 67 | | SignalAndWait(CancellationToken) | 发出参与者已达到屏障的信号,并等待所有其他参与者达到屏障,同时观察取消标记。 | 68 | | SignalAndWait(Int32) | 发出参与者已达到屏障的信号,并等待所有其他参与者也达到屏障,同时使用 32 位带符号整数测量超时。 | 69 | | SignalAndWait(Int32, CancellationToken) | 发出参与者已达到屏障的信号,并等待所有其他参与者也达到屏障,使用 32 位带符号整数测量超时,同时观察取消标记。 | 70 | | SignalAndWait(TimeSpan) | 发出参与者已达到屏障的信号,并等待所有其他参与者也达到屏障,同时使用 TimeSpan 对象测量时间间隔。 | 71 | | SignalAndWait(TimeSpan, CancellationToken) | 发出参与者已达到屏障的信号,并等待所有其他参与者也达到屏障,使用 TimeSpan 对象测量时间间隔,同时观察取消标记。 | 72 | 73 | Barrier 翻译屏障,前面所说的 “阶段”,在文档中称为屏障,官方有一些例子和实践场景: 74 | 75 | **https://docs.microsoft.com/zh-cn/dotnet/standard/threading/barrier?view=netcore-3.1** 76 | 77 | https://docs.microsoft.com/zh-cn/dotnet/standard/threading/how-to-synchronize-concurrent-operations-with-a-barrier?view=netcore-3.1 78 | 79 | 本文的教程比较简单,你可以先看本教程,再去看看官方示例。 80 | 81 | 82 | 83 | ### 示例 84 | 85 | 假设有个比赛,一个有三个环节,有三个小组参加比赛。 86 | 87 | 比赛有三个环节,小组完成一个环节后,可以去等待区休息,等待其他小组也完成比赛后,开始进行下一个环节的比赛。 88 | 89 | 示例如下: 90 | 91 | `new Barrier(int,Action)` 设置有多少线程参与,Action 委托设置每个阶段完成后执行哪些动作。 92 | 93 | `.SignalAndWait()` 阻止当前线程继续往下执行;直到其他完成也执行到此为止。 94 | 95 | ```csharp 96 | class Program 97 | { 98 | // Barrier(Int32, Action) 99 | private static Barrier barrier = new Barrier(3, b => 100 | Console.WriteLine($"\n第 {b.CurrentPhaseNumber + 1} 环节的比赛结束,请评分!")); 101 | 102 | static void Main(string[] args) 103 | { 104 | // Random 模拟每个小组完成一个环节比赛需要的时间 105 | Thread thread1 = new Thread(() => DoWork("第一小组", new Random().Next(2, 10))); 106 | Thread thread2 = new Thread(() => DoWork("第二小组", new Random().Next(2, 10))); 107 | Thread thread3 = new Thread(() => DoWork("第三小组", new Random().Next(2, 10))); 108 | 109 | // 三个小组开始比赛 110 | thread1.Start(); 111 | thread2.Start(); 112 | thread3.Start(); 113 | 114 | 115 | Console.ReadKey(); 116 | } 117 | static void DoWork(string name, int seconds) 118 | { 119 | // 第一环节 120 | Console.WriteLine($"\n{name}:开始进入第一环节比赛"); 121 | Thread.Sleep(TimeSpan.FromSeconds(seconds)); // 模拟小组完成环节比赛需要的时间 122 | Console.WriteLine($"\n {name}:完成第一环节比赛,等待其它小组"); 123 | // 小组完成阶段任务,去休息等待其它小组也完成比赛 124 | barrier.SignalAndWait(); 125 | 126 | // 第二环节 127 | Console.WriteLine($"\n {name}:开始进入第二环节比赛"); 128 | Thread.Sleep(TimeSpan.FromSeconds(seconds)); 129 | Console.WriteLine($"\n {name}:完成第二环节比赛,等待其它小组\n"); 130 | barrier.SignalAndWait(); 131 | 132 | 133 | // 第三环节 134 | Console.WriteLine($"\n {name}:开始进入第三环节比赛"); 135 | Thread.Sleep(TimeSpan.FromSeconds(seconds)); 136 | Console.WriteLine($"\n {name}:完成第三环节比赛,等待其它小组\n"); 137 | barrier.SignalAndWait(); 138 | } 139 | } 140 | ``` 141 | 142 | ![并行协调](images/并行协调.gif) 143 | 144 | 上面的示例中,每个线程都使用了 `DoWork()` 这个方法去中相同的事情,当然也可以设置多个线程执行不同的任务,但是必须保证每个线程都具有相同数量的 `.SignalAndWait();` 方法。 145 | 146 | 当然 `SignalAndWait()` 可以设置等待时间,如果其他线程迟迟没有到这一步,那就继续运行。可以避免死锁等问题。 147 | 148 | 到目前,只使用了 `SignalAndWait()` ,我们继续学习一下 Barrier 类的其他方法。 149 | 150 | 151 | 152 | ### 线程淘汰 153 | 154 | `Barrier.AddParticipant()`:添加参与者; 155 | 156 | `Barrier.RemoveParticipant()`:移除参与者; 157 | 158 | 159 | 160 | 因为这是比赛,老是等待其他小组,会使得比赛进行比较慢。这里继续使用第二节的示例,当发现有些线程执行中出现特殊情况时,可以移除这些线程,让正常的线程继续执行下去。 161 | 162 | 新的规则:不必等待最后一名,当环节只剩下最后一名时为完成时,其它小组可以立即进行下一个环节的比赛。 163 | 164 | ​ 当然,最后一名小组,有权利继续完成比赛。 165 | 166 | 修改第二小节的代码,在 Main 内第一行加上 `barrier.RemoveParticipant();`。 167 | 168 | ```csharp 169 | static void Main(string[] args) 170 | { 171 | barrier.RemoveParticipant(); 172 | ... ... 173 | ``` 174 | 175 | 试着再运行一下。 176 | 177 | 178 | 179 | ### 说明 180 | 181 | `SignalAndWait()` 的 重载比较多,例如 `SignalAndWait(CancellationToken)`,这里笔者先不讲解此方法如何使用。等到写到后面的异步(`Task`),读者学到相关的知识点,我们再过一次复习,这样由易到难,自然水到渠成。 182 | 183 | Barrier 适合用于同时执行相同流程的工作,因为工作内容是相同的,便于协同。工作流有可能用得上吧。 184 | 185 | 但是 Barrier 更加适合用于算法领域,可以参考:https://devblogs.microsoft.com/pfxteam/parallel-merge-sort-using-barrier/ 186 | 187 | 当然,后面学习异步和并行编程后,也会编写相应的算法示例。 188 | -------------------------------------------------------------------------------- /2.thread_sync/5.autorestevent.md: -------------------------------------------------------------------------------- 1 | # 2.4 线程通知 2 | 3 | ### 导读 4 | 5 | 回顾一下,前面 lock、Monitor 部分我们学习了线程锁,Mutex 部分学习了进程同步,Semaphor 部分学习了资源池限制。 6 | 7 | 这一篇将学习 C# 中用于发送线程通知的 AutoRestEvent 类。 8 | 9 | ![image-20220326092016946](images/image-20220326092016946.png) 10 | 11 | 【图来自《C# 7.0 核心技术指南》】 12 | 13 | 14 | 15 | ## AutoRestEvent 类 16 | 17 | 用于从一个线程向另一个线程发送通知。 18 | 19 | 微软文档是这样介绍的:表示线程同步事件在一个等待线程释放后收到信号时自动重置。 20 | 21 | 22 | 23 | 其构造函数只有一个: 24 | 25 | 构造函数里面的参数用于设置信号状态。 26 | 27 | | 构造函数 | 说明 | 28 | | ----------------------- | ------------------------------------------------------------ | 29 | | AutoResetEvent(Boolean) | 用一个指示是否将初始状态设置为终止的布尔值初始化 AutoResetEvent 类的新实例。 | 30 | 31 | > 真糟糕的机器翻译。 32 | 33 | 34 | 35 | ### 常用方法 36 | 37 | AutoRestEvent 类是干嘛的,构造函数的参数又是干嘛的?不着急,我们来先来看看这个类常用的方法: 38 | 39 | | 方法 | 说明 | 40 | | ------- | ------------------------------------------------------------ | 41 | | Close() | 释放由当前 WaitHandle 占用的所有资源。 | 42 | | Reset() | 将事件状态设置为非终止,从而导致线程受阻。 | 43 | | Set() | 将事件状态设置为有信号,从而允许一个或多个等待线程继续执行。 | 44 | | WaitOne() | 阻止当前线程,直到当前 WaitHandle 收到信号。 | 45 | | WaitOne(Int32) | 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 | 46 | | WaitOne(Int32, Boolean) | 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 | 47 | | WaitOne(TimeSpan) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。 | 48 | | WaitOne(TimeSpan, Boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。 | 49 | 50 | 51 | 52 | ### 一个简单的示例 53 | 54 | 这里我们编写一个这样的程序: 55 | 56 | 创建一个线程,能够执行多个阶段的任务;每完成一个阶段,都需要停下来,等待子线程发生通知,才能继续下一步执行。 57 | 58 | `.WaitOne()` 用来等待另一个线程发送通知; 59 | 60 | `.Set()` 用来对线程发出通知,此时 `AutoResetEvent` 变成终止状态; 61 | 62 | `.ReSet()` 用来重置 `AutoResetEvent` 状态; 63 | 64 | 65 | 66 | ```csharp 67 | class Program 68 | { 69 | // 线程通知 70 | private static AutoResetEvent resetEvent = new AutoResetEvent(false); 71 | 72 | static void Main(string[] args) 73 | { 74 | // 创建线程 75 | new Thread(DoOne).Start(); 76 | 77 | // 用于不断向另一个线程发送信号 78 | while (true) 79 | { 80 | Console.ReadKey(); 81 | resetEvent.Set(); // 发生通知,设置终止状态 82 | } 83 | } 84 | 85 | public static void DoOne() 86 | { 87 | Console.WriteLine("等待中,请发出信号允许我运行"); 88 | 89 | // 等待其它线程发送信号 90 | resetEvent.WaitOne(); 91 | 92 | Console.WriteLine("\n 收到信号,继续执行"); 93 | for (int i = 0; i < 5; i++) Thread.Sleep(TimeSpan.FromSeconds(0.5)); 94 | 95 | resetEvent.Reset(); // 重置为非终止状态 96 | Console.WriteLine("\n第一阶段运行完毕,请继续给予指示"); 97 | 98 | // 等待其它线程发送信号 99 | resetEvent.WaitOne(); 100 | Console.WriteLine("\n 收到信号,继续执行"); 101 | for (int i = 0; i < 5; i++) Thread.Sleep(TimeSpan.FromSeconds(0.5)); 102 | 103 | Console.WriteLine("\n第二阶段运行完毕,线程结束,请手动关闭窗口"); 104 | } 105 | } 106 | ``` 107 | 108 | ![自动线程通知](images/自动线程通知.gif) 109 | 110 | 111 | 112 | 在这个过程中,出现了主线程、子线程协调,共同完成任务。 113 | 114 | 115 | 116 | ### 解释一下 117 | 118 | AutoResetEvent 对象有终止和非终止状态。`Set()` 设置终止状态,`Reset()` 重置非终止状态。 119 | 120 | 这个终止状态,可以理解成信号已经通知;非终止状态则是信号还没有通知。 121 | 122 | 注意,注意终止状态和非终止状态指的是 AutoResetEvent 的状态,不是指线程的状态。 123 | 124 |

125 |

126 | 线程通过调用 WaitOne() 方法,等待信号;
127 | 另一个线程可以调用 Set() 通知 AutoResetEvent 释放等待线程。
128 | 然后 AutoResetEvent 变为终止状态。 129 |
130 |

131 | 132 | 需要注意的是,如果 AutoResetEvent 已经处于终止状态,那么线程调用 `WaitOne()` 不会再起作用。除非调用`Reset()` 。 133 | 134 | 135 | 136 | 构造函数中的参数,正是设置这个状态的。true 代表终止状态,false 代表非终止状态。如果使用 `new AutoResetEvent(true);` ,则线程一开始是无需等待信号的。 137 | 138 | 在使用完类型后,您应直接或间接释放类型,显式调用 `Close()/Dispose()` 或 使用 `using`。 当然,也可以直接退出程序。 139 | 140 | 141 | 142 | 需要注意的是,如果多次调用 `Set()` 的时间间隔过短,如果第一次 `Set()` 还没有结束(信号发送需要处理时间),那么第二次 `Set()` 可能无效(不起作用)。 143 | 144 | 145 | 146 | ### 复杂一点的示例 147 | 148 | 我们设计一个程序: 149 | 150 | * Two 线程开始处于阻塞状态; 151 | * 线程 One 可以设置线程 Two 继续运行,然后阻塞自己; 152 | * 线程 Two 可以设置 One 继续运行,然后阻塞自己; 153 | 154 | ![file](./images/image-1587256361021.png) 155 | 156 | 157 | 158 | 程序代码如下(运行后,请将键盘设置成英文输入状态再按下按键): 159 | 160 | 161 | 162 | ```csharp 163 | class Program 164 | { 165 | // 控制第一个线程 166 | // 第一个线程开始时,AutoResetEvent 处于终止状态,无需等待信号 167 | private static AutoResetEvent oneResetEvent = new AutoResetEvent(true); 168 | 169 | // 控制第二个线程 170 | // 第二个线程开始时,AutoResetEvent 处于非终止状态,需要等待信号 171 | private static AutoResetEvent twoResetEvent = new AutoResetEvent(false); 172 | 173 | static void Main(string[] args) 174 | { 175 | new Thread(DoOne).Start(); 176 | new Thread(DoTwo).Start(); 177 | 178 | Console.ReadKey(); 179 | } 180 | 181 | public static void DoOne() 182 | { 183 | while (true) 184 | { 185 | Console.WriteLine("\n① 按一下键,我就让DoTwo运行"); 186 | Console.ReadKey(); 187 | twoResetEvent.Set(); 188 | oneResetEvent.Reset(); 189 | // 等待 DoTwo() 给我信号 190 | oneResetEvent.WaitOne(); 191 | 192 | Console.ForegroundColor = ConsoleColor.Green; 193 | Console.WriteLine("\n DoOne() 执行"); 194 | Console.ForegroundColor = ConsoleColor.White; 195 | } 196 | } 197 | 198 | public static void DoTwo() 199 | { 200 | while (true) 201 | { 202 | Thread.Sleep(TimeSpan.FromSeconds(1)); 203 | 204 | // 等待 DoOne() 给我信号 205 | twoResetEvent.WaitOne(); 206 | 207 | Console.ForegroundColor = ConsoleColor.Yellow; 208 | Console.WriteLine("\n DoTwo() 执行"); 209 | Console.ForegroundColor = ConsoleColor.White; 210 | 211 | Console.WriteLine("\n② 按一下键,我就让DoOne运行"); 212 | Console.ReadKey(); 213 | oneResetEvent.Set(); 214 | twoResetEvent.Reset(); 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | ![线程通知](./images/线程通知.gif) 221 | 222 | 223 | 224 | ### 解释 225 | 226 | 两个线程具有的功能:阻塞自己、解除另一个线程的阻塞。 227 | 228 | 用电影《最佳拍档》里面的一个画面来理解。 229 | 230 | DoOne 、DoTwo 轮流呼吸,不能自己控制自己呼吸,但自己能够决定别人呼吸。 231 | 232 | 你搞我,我搞你,就能相互呼吸了。 233 | 234 | ![file](./images/image-1586684447732.png) 235 | 236 | 237 | 238 | 当然`WaitOne()` 也可以设置等待时间,如果 光头佬(DoOne) 耍赖不让 金刚(DoTwo)呼吸,金刚等待一定时间后,**可以强行荡动天平,落地呼吸**。 239 | 240 |

241 |

242 | 注意,AutoRestEvent 用得不当容易发生死锁。 243 |
244 | 另外 AutoRestEvent 使用的是内核时间模式,因此等待时间不能太长,不然比较耗费 CPU 时间。 245 |
246 |

247 | 248 | 249 | AutoResetEvent 也适合用于线程同步。 -------------------------------------------------------------------------------- /3.task/8.async_state.md: -------------------------------------------------------------------------------- 1 | # 3.8 编写异步状态机 2 | 3 | 在前面,笔者强调过很多次,async、await 只是语法糖,当 .NET SDK 编译 C# 代码时,async、await 会被转换为对应的代码,然后再编译成 IL 代码。 4 | 那么在本章中,笔者将为大家介绍编译器去掉 async、await 的之后的代码,我们又应该如何编写应该这样的状态机,以便加深大家对 Task 任务调度的理解。 5 | 6 | ### async、await 变成了什么 7 | 8 | 下面是一段很简单的代码: 9 | 10 | 11 | ```csharp 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | 15 | namespace ConsoleApp1 16 | { 17 | public class Program 18 | { 19 | static async Task Main(string[] args) 20 | { 21 | var p = new Program(); 22 | 23 | // 这段代码被转换为状态机 24 | var result = await p.GetAsync(111); 25 | } 26 | 27 | public async Task GetAsync(int id) 28 | { 29 | Thread.Sleep(1000); 30 | await Task.CompletedTask; 31 | return 1; 32 | } 33 | } 34 | } 35 | ``` 36 | 37 | 如果我们去掉语法糖,那么这个代码会是什么样子呢? 38 | 我们可以打开 https://sharplab.io/ ,将代码填充进去。 39 | 40 | ![csharp](./images/3801.png) 41 | 42 | 可以看到,如果去掉语法糖,生成的代码竟然如此复杂。 43 | 44 | 45 | ### 实现异步状态机 46 | 47 | 在本小节,我们将会实现一个异步状态机。 48 | 49 | 首先是异步状态机的接口 `IAsyncStateMachine` 定义如下: 50 | 表示为异步方法生成的状态机。此类型仅供编译器使用。 51 | 52 | ```csharp 53 | using System.Runtime.CompilerServices; 54 | 55 | public interface IAsyncStateMachine 56 | { 57 | // 将状态机移动到下一个状态 58 | void MoveNext(); 59 | 60 | // 使用堆分配的副本配置状态机 61 | void SetStateMachine(IAsyncStateMachine stateMachine); 62 | } 63 | ``` 64 | 65 | 创建一个名为 GGG 的结构体。 66 | 67 | ```csharp 68 | [CompilerGenerated] 69 | public struct GGG : IAsyncStateMachine 70 | { 71 | } 72 | ``` 73 | 74 | 75 | 为了便于理解,我们首先只需要将下面这段代码转换为异步状态机的写法即可: 76 | 77 | ```csharp 78 | var result = await p.GetAsync(111); 79 | ``` 80 | 81 | 82 | 在实现状态机时,首先要提取两个信息: 83 | 1,调用者对象 84 | 2,调用方法传递的参数 85 | 86 | ```csharp 87 | [CompilerGenerated] 88 | public struct GGG : IAsyncStateMachine 89 | { 90 | public Program __this; 91 | public int id; 92 | } 93 | ``` 94 | 95 | 然后在 Main 方法中,可以这样实例化状态机: 96 | 97 | ```csharp 98 | // p. (111); 99 | GGG g = new GGG() 100 | { 101 | __this = p, 102 | id = 111 103 | }; 104 | ``` 105 | 106 | 107 | 因为我们要调用一个异步方法,所以我们需要将这个方法放到异步任务队列之中,并且能够知道任务是否完成。 108 | 109 | 这个时候就需要使用 `AsyncTaskMethodBuilder`, 表示返回任务的异步方法的生成器,其中里面的泛型指的是返回类型。 110 | 111 | 112 | 113 | ```csharp 114 | 115 | [CompilerGenerated] 116 | public struct GGG : IAsyncStateMachine 117 | { 118 | // 调用方和参数 119 | public Program __this; 120 | public int id; 121 | 122 | // 异步方法生成器 123 | public AsyncTaskMethodBuilder __builder; 124 | } 125 | ``` 126 | > 你可以使用结构体,也可以使用类,效果都是一样的。 127 | 128 | 创建异步方法调用器。 129 | ```csharp 130 | static async Task Main(string[] args) 131 | { 132 | var p = new Program(); 133 | 134 | // 这段代码被转换为状态机 135 | // await p.GetAsync(111); 136 | 137 | GGG g = new GGG() 138 | { 139 | __this = p, 140 | id = 111 141 | }; 142 | 143 | g.__builder = AsyncTaskMethodBuilder.Create(); 144 | } 145 | ``` 146 | 147 | 接着,调用方法和获取返回结果,我们需要多加三个字段: 148 | 149 | ```csharp 150 | [CompilerGenerated] 151 | public struct GGG : IAsyncStateMachine 152 | { 153 | // 调用方和参数 154 | public Program __this; 155 | public int id; 156 | 157 | // 异步方法生成器 158 | public AsyncTaskMethodBuilder __builder; 159 | 160 | // 异步任务的状态 161 | public int __state; 162 | 163 | // 要调用的异步方法 164 | private TaskAwaiter __task1Awaiter; 165 | 166 | // 调用方法返回的值 167 | private int result; 168 | } 169 | ``` 170 | 171 | 172 | 接下来我们要实现状态机的 `MoveNext()` 方法。 173 | 174 | 175 | ```csharp 176 | [CompilerGenerated] 177 | public struct GGG : IAsyncStateMachine 178 | { 179 | // 调用方和参数 180 | public Program __this; 181 | public int id; 182 | 183 | // 异步方法生成器 184 | public AsyncTaskMethodBuilder __builder; 185 | 186 | // 异步任务的状态 187 | public int __state; 188 | 189 | // 要调用的异步方法 190 | private TaskAwaiter __task1Awaiter; 191 | 192 | // 调用方法返回的值 193 | private int result; 194 | 195 | // 196 | public void MoveNext() 197 | { 198 | try 199 | { 200 | TaskAwaiter awaiter; 201 | if (__state != 0) 202 | { 203 | if (id == 0) throw new ArgumentNullException(nameof(id)); 204 | 205 | // 要调用的方法,此时方法已经开始被执行 206 | awaiter = __this.GetAsync(id).GetAwaiter(); 207 | 208 | // 如果该方法还没有执行完成,则开始调度到任务队列 209 | if (!awaiter.IsCompleted) 210 | { 211 | __state = 0; 212 | __task1Awaiter = awaiter; 213 | // 没有完成的话,放到后台完成 214 | __builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); 215 | return; 216 | } 217 | } 218 | // 如果该方法已经被执行完成 219 | else 220 | { 221 | awaiter = __task1Awaiter; 222 | __task1Awaiter = default(TaskAwaiter); 223 | __state = -1; 224 | } 225 | result = awaiter.GetResult(); 226 | } 227 | catch (Exception ex) 228 | { 229 | __state = -2; 230 | __builder.SetException(ex); 231 | return; 232 | } 233 | 234 | __state = -2; 235 | __builder.SetResult(result); 236 | } 237 | 238 | 239 | [DebuggerHidden] 240 | void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) 241 | { 242 | __builder.SetStateMachine(stateMachine); 243 | } 244 | } 245 | ``` 246 | 247 | 接着,我们便可以使用 `AsyncTaskMethodBuilder.Start()` 执行异步方法了,`.Start()` 被调用时,开始使用关联的状态机运行生成器。 248 | > `Start(TStateMachine)` 。 249 | 250 | ```csharp 251 | static async Task Main(string[] args) 252 | { 253 | var p = new Program(); 254 | 255 | // 这段代码被转换为状态机 256 | // await p.GetAsync(111); 257 | 258 | GGG g = new GGG() 259 | { 260 | __this = p, 261 | id = 111 262 | }; 263 | g.__builder = AsyncTaskMethodBuilder.Create(); 264 | g.__state = -1; 265 | 266 | // 这里开始调用 267 | g.__builder.Start(ref g); 268 | var task = g.__builder.Task; 269 | } 270 | 271 | ``` 272 | 273 | `AsyncTaskMethodBuilder.Start()` 的核心功能是,设置线程上下文,并执行我们状态机的 `MoveNext()` 方法。 274 | 275 | ```csharp 276 | [DebuggerStepThrough] 277 | public static void Start(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine 278 | { 279 | if (stateMachine == null) 280 | { 281 | ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine); 282 | } 283 | Thread currentThread = Thread.CurrentThread; 284 | ExecutionContext executionContext = currentThread._executionContext; 285 | SynchronizationContext synchronizationContext = currentThread._synchronizationContext; 286 | try 287 | { 288 | // 这里是我们状态机的方法 289 | stateMachine.MoveNext(); 290 | } 291 | finally 292 | { 293 | if (synchronizationContext != currentThread._synchronizationContext) 294 | { 295 | currentThread._synchronizationContext = synchronizationContext; 296 | } 297 | ExecutionContext executionContext2 = currentThread._executionContext; 298 | if (executionContext != executionContext2) 299 | { 300 | ExecutionContext.RestoreChangedContextToThread(currentThread, executionContext, executionContext2); 301 | } 302 | } 303 | } 304 | 305 | ``` 306 | 307 | 当程序执行 `AsyncTaskMethodBuilder.Start()` 时,当前线程就会被挂起,当方法执行完毕后,才会执行后面的代码。 308 | ```csharp 309 | g.__builder.Start(ref g); 310 | ``` 311 | 312 | 完整代码示例如下。 313 | 314 | ```csharp 315 | namespace ConsoleApp1 316 | { 317 | public class Program 318 | { 319 | static async Task Main(string[] args) 320 | { 321 | var p = new Program(); 322 | 323 | // 这段代码被转换为状态机 324 | // await p.GetAsync(111); 325 | 326 | GGG g = new GGG() 327 | { 328 | __this = p, 329 | id = 111 330 | }; 331 | g.__builder = AsyncTaskMethodBuilder.Create(); 332 | g.__state = -1; 333 | 334 | // 这里会等待方法执行完成 335 | g.__builder.Start(ref g); 336 | 337 | // 方法执行完成后才到这里 338 | var task = g.__builder.Task; 339 | var result = task.Result; 340 | } 341 | 342 | public async Task GetAsync(int id) 343 | { 344 | Thread.Sleep(10000); 345 | await Task.CompletedTask; 346 | return 1; 347 | } 348 | } 349 | 350 | [CompilerGenerated] 351 | public struct GGG : IAsyncStateMachine 352 | { 353 | // 调用方和参数 354 | public Program __this; 355 | public int id; 356 | 357 | // 异步方法生成器 358 | public AsyncTaskMethodBuilder __builder; 359 | 360 | // 异步任务的状态 361 | public int __state; 362 | 363 | // 要调用的异步方法 364 | private TaskAwaiter __task1Awaiter; 365 | 366 | // 调用方法返回的值 367 | private int result; 368 | 369 | // 370 | public void MoveNext() 371 | { 372 | try 373 | { 374 | TaskAwaiter awaiter; 375 | if (__state != 0) 376 | { 377 | if (id == 0) throw new ArgumentNullException(nameof(id)); 378 | 379 | // 要调用的方法,此时方法已经开始被执行 380 | awaiter = __this.GetAsync(id).GetAwaiter(); 381 | 382 | // 如果该方法还没有执行完成,则开始调度到任务队列 383 | if (!awaiter.IsCompleted) 384 | { 385 | __state = 0; 386 | __task1Awaiter = awaiter; 387 | // 没有完成的话,放到后台完成 388 | __builder.AwaitUnsafeOnCompleted(ref awaiter, ref this); 389 | return; 390 | } 391 | } 392 | // 如果该方法已经被执行完成 393 | else 394 | { 395 | awaiter = __task1Awaiter; 396 | __task1Awaiter = default(TaskAwaiter); 397 | __state = -1; 398 | } 399 | result = awaiter.GetResult(); 400 | } 401 | catch (Exception ex) 402 | { 403 | __state = -2; 404 | __builder.SetException(ex); 405 | return; 406 | } 407 | 408 | __state = -2; 409 | __builder.SetResult(result); 410 | } 411 | 412 | 413 | [DebuggerHidden] 414 | void IAsyncStateMachine.SetStateMachine(IAsyncStateMachine stateMachine) 415 | { 416 | __builder.SetStateMachine(stateMachine); 417 | } 418 | } 419 | 420 | } 421 | ``` 422 | 423 | -------------------------------------------------------------------------------- /2.thread_sync/2.locker_monitor.md: -------------------------------------------------------------------------------- 1 | # 2.2 Locker 和 Monitor 锁 2 | 3 | ### 导读 4 | 5 | C# 中,可以使用 lock 关键字和 Monitor 类来解决多线程锁定资源和死锁的问题。 6 | 7 |

8 |

9 | 官方解释:lock 语句获取给定对象的互斥 lock,执行语句块,然后释放 lock。 10 |
11 |

12 | 13 | 下面我们将来探究 lock 关键字和 Monitor 类的使用。 14 | 15 | 16 | 17 | ## Lock 18 | 19 | lock 用于读一个引用类型进行加锁,同一时刻内只有一个线程能够访问此对象。lock 是语法糖,是通过 Monitor 来实现的。 20 | 21 | Lock 锁定的对象,应该是静态的引用类型(字符串除外)。 22 | 23 |

24 |

25 | 实际上字符串也可以作为锁的对象使用,只是由于字符串对象的特殊性,可能会造成不同位置的不同线程冲突。
26 | 如果你能保证字符串的唯一性,例如 Guid 生成的字符串,也是可以作为锁的对象使用的(但不建议)。 27 |
锁的对象也不一定要静态才行,也可以通过类实例的成员变量,作为锁对象。 28 |
29 |

30 | 31 | 32 | 33 | ### lock 原型 34 | 35 | lock 是 Monitor 的语法糖,生成的代码对比: 36 | 37 | ```csharp 38 | lock (x) 39 | { 40 | // Your code... 41 | } 42 | ``` 43 | 44 | ```csharp 45 | object __lockObj = x; 46 | bool __lockWasTaken = false; 47 | try 48 | { 49 | System.Threading.Monitor.Enter(__lockObj, ref __lockWasTaken); 50 | // Your code... 51 | } 52 | finally 53 | { 54 | if (__lockWasTaken) System.Threading.Monitor.Exit(__lockObj); 55 | } 56 | ``` 57 | 58 | 这里先不理会 Monitor,后面再说。 59 | 60 | 61 | 62 | ### lock 编写实例 63 | 64 | 首先,如果像下面这样写的话,拉出去打 si 吧。 65 | 66 | ```csharp 67 | public void MyLock() 68 | { 69 | object o = new object(); 70 | lock (o) 71 | { 72 | // 73 | } 74 | } 75 | ``` 76 | 77 | ![file](./images/image-1587130109424.png) 78 | 79 | 80 | 81 | 下面编写一个简单的锁,示例如下: 82 | 83 | ```csharp 84 | class Program 85 | { 86 | private static object obj = new object(); 87 | private static int sum = 0; 88 | static void Main(string[] args) 89 | { 90 | Thread thread1 = new Thread(Sum1); 91 | thread1.Start(); 92 | Thread thread2 = new Thread(Sum2); 93 | thread2.Start(); 94 | while (true) 95 | { 96 | Console.WriteLine($"{DateTime.Now.ToString()}:" + sum); 97 | Thread.Sleep(TimeSpan.FromSeconds(1)); 98 | } 99 | } 100 | 101 | public static void Sum1() 102 | { 103 | sum = 0; 104 | lock (obj) 105 | { 106 | for (int i = 0; i < 10; i++) 107 | { 108 | sum += i; 109 | Console.WriteLine("Sum1"); 110 | Thread.Sleep(TimeSpan.FromSeconds(2)); 111 | } 112 | } 113 | } 114 | 115 | public static void Sum2() 116 | { 117 | sum = 0; 118 | lock (obj) 119 | { 120 | for (int i = 0; i < 10; i++) 121 | { 122 | sum += 1; 123 | Console.WriteLine("Sum2"); 124 | Thread.Sleep(TimeSpan.FromSeconds(2)); 125 | } 126 | } 127 | } 128 | } 129 | ``` 130 | 131 | 132 | 133 | 类将自己设置为锁, 这可以防止恶意代码对公共对象采用做锁。 134 | 135 | 例如: 136 | 137 | ```csharp 138 | public void Access() 139 | { 140 | lock(this) {} 141 | } 142 | ``` 143 | 144 | 145 | 146 |

147 |

148 | 锁可以阻止其它线程执行锁块(lock(o){})中的代码,当锁定时,其它线程必须等待锁中的线程执行完成并释放锁。但是这可能会给程序带来性能影响。
锁不太适合I/O场景,例如文件I/O,繁杂的计算或者操作比较持久的过程,会给程序带来很大的性能损失。 149 |
150 |

151 | 152 | 10 种优化锁的性能方法: [http://www.thinkingparallel.com/2007/07/31/10-ways-to-reduce-lock-contention-in-threaded-programs/](http://www.thinkingparallel.com/2007/07/31/10-ways-to-reduce-lock-contention-in-threaded-programs/) 153 | 154 | 155 | 156 | 157 | 158 | ## Monitor 159 | 160 | 此对象提供同步访问对象的机制;Monotor 是一个静态类型,其方法比较少,常用方法如下: 161 | 162 | | 操作 | 说明 | 163 | | :--------------- | :----------------------------------------------------------- | 164 | | Enter, TryEnter | 获取对象的锁。 此操作还标记关键节的开头。 其他任何线程都不能输入临界区,除非它使用不同的锁定对象执行临界区中的说明。 | 165 | | Wait | 释放对象的锁,以允许其他线程锁定并访问对象。 调用线程会等待另一个线程访问对象。 使用脉冲信号通知等待线程关于对象状态的更改。 | 166 | | Pulse 、PulseAll | 将信号发送到一个或多个等待线程。 信号通知等待线程:锁定对象的状态已更改,锁的所有者已准备好释放该锁。 正在等待的线程置于对象的就绪队列中,因此它可能最终接收对象的锁。 线程锁定后,它可以检查对象的新状态,以查看是否已达到所需的状态。 | 167 | | Exit | 释放对象的锁。 此操作还标记受锁定对象保护的临界区的结尾。 | 168 | 169 | 170 | 171 | ### 怎么用呢 172 | 173 | 下面是一个很简单的示例: 174 | 175 | ```csharp 176 | private static object obj = new object(); 177 | private static bool acquiredLock = false; 178 | 179 | public static void Test() 180 | { 181 | try 182 | { 183 | Monitor.Enter(obj, ref acquiredLock); 184 | } 185 | catch { } 186 | finally 187 | { 188 | if (acquiredLock) 189 | Monitor.Exit(obj); 190 | } 191 | } 192 | ``` 193 | 194 | `Monitor.Enter` 锁定 obj 这个对象,并且设置 acquiredLock 为 true,告诉别人 obj 已经被锁定。 195 | 196 | 最后结束时,判断 acquiredLock ,释放锁,并设置 acquiredLock 为 false。 197 | 198 | 199 | 200 | ### 解释一下 201 | 202 | 临界区:指被某些符号包围的范围。例如 `{}` 内。 203 | 204 | Monitor 对象的 Enter 和 Exit 方法来标记临界区的开头和结尾。 205 | 206 | `Enter()` 方法获取锁后,能够保证只有单个线程能够使用临界区中的代码。使用 Monitor 类,最好搭配 `try{...}catch{...}finally{...}` 来使用,因为如果获取到锁但是没有释放锁的话,会导致其它线程无限阻塞,即发生死锁。 207 | 208 | 一般来说,lock 关键字够用了。 209 | 210 | 211 | 212 | 213 | 214 | ### 示例 215 | 216 | 下面示范了多个线程如何使用 Monitor 来实现锁: 217 | 218 | ```csharp 219 | private static object obj = new object(); 220 | private static bool acquiredLock = false; 221 | static void Main(string[] args) 222 | { 223 | new Thread(Test1).Start(); 224 | Thread.Sleep(1000); 225 | new Thread(Test2).Start(); 226 | } 227 | 228 | public static void Test1() 229 | { 230 | try 231 | { 232 | Monitor.Enter(obj, ref acquiredLock); 233 | for (int i = 0; i < 10; i++) 234 | { 235 | Console.WriteLine("Test1正在锁定资源"); 236 | Thread.Sleep(1000); 237 | } 238 | 239 | } 240 | catch { } 241 | finally 242 | { 243 | if (acquiredLock) 244 | Monitor.Exit(obj); 245 | Console.WriteLine("Test1已经释放资源"); 246 | } 247 | } 248 | public static void Test2() 249 | { 250 | bool isGetLock = false; 251 | Monitor.Enter(obj); 252 | try 253 | { 254 | Monitor.Enter(obj, ref acquiredLock); 255 | for (int i = 0; i < 10; i++) 256 | { 257 | Console.WriteLine("Test2正在锁定资源"); 258 | Thread.Sleep(1000); 259 | } 260 | 261 | } 262 | catch { } 263 | finally 264 | { 265 | if (acquiredLock) 266 | Monitor.Exit(obj); 267 | Console.WriteLine("Test2已经释放资源"); 268 | } 269 | } 270 | ``` 271 | 272 | 273 | 274 | 275 | 276 | ### 设置获取锁的时效 277 | 278 | 如果对象已经被锁定,另一个线程使用 `Monitor.Enter` 对象,就会一直等待另一个线程解除锁定。 279 | 280 | 但是,如果一个线程发生问题或者出现死锁的情况,锁一直被锁定呢?或者线程具有时效性,超过一段时间不执行,已经没有了意义呢? 281 | 282 | 我们可以通过 `Monitor.TryEnter()` 来设置等待时间,超过一段时间后,如果锁还没有释放,就会返回 false。 283 | 284 | 改造上面的示例如下: 285 | 286 | ```csharp 287 | private static object obj = new object(); 288 | private static bool acquiredLock = false; 289 | static void Main(string[] args) 290 | { 291 | new Thread(Test1).Start(); 292 | Thread.Sleep(1000); 293 | new Thread(Test2).Start(); 294 | } 295 | 296 | public static void Test1() 297 | { 298 | try 299 | { 300 | Monitor.Enter(obj, ref acquiredLock); 301 | for (int i = 0; i < 10; i++) 302 | { 303 | Console.WriteLine("Test1正在锁定资源"); 304 | Thread.Sleep(1000); 305 | } 306 | } 307 | catch { } 308 | finally 309 | { 310 | if (acquiredLock) 311 | Monitor.Exit(obj); 312 | Console.WriteLine("Test1已经释放资源"); 313 | } 314 | } 315 | public static void Test2() 316 | { 317 | bool isGetLock = false; 318 | isGetLock = Monitor.TryEnter(obj, 500); 319 | if (isGetLock == false) 320 | { 321 | Console.WriteLine("锁还没有释放,我不干活了"); 322 | return; 323 | } 324 | try 325 | { 326 | Monitor.Enter(obj, ref acquiredLock); 327 | for (int i = 0; i < 10; i++) 328 | { 329 | Console.WriteLine("Test2正在锁定资源"); 330 | Thread.Sleep(1000); 331 | } 332 | } 333 | catch { } 334 | finally 335 | { 336 | if (acquiredLock) 337 | Monitor.Exit(obj); 338 | Console.WriteLine("Test2已经释放资源"); 339 | } 340 | } 341 | ``` 342 | 343 | 344 | 345 | 对于锁的使用,还有很多高级复杂的技术,本文简单地介绍了 Lock 和 Monitor 的使用。 346 | 347 | 随着教程的深入,会继续学习很多高级的使用方法。 348 | 349 | 350 | 351 | ## 方法锁 352 | 353 | `[MethodImpl(MethodImplOptions.Synchronized)]` 特性标记,可以让该方法只允许同时一个线程运行。 354 | 355 | ```csharp 356 | [MethodImpl(MethodImplOptions.Synchronized)] 357 | public void Test() 358 | { 359 | 360 | } 361 | ``` 362 | 363 | -------------------------------------------------------------------------------- /2.thread_sync/4.semaphore.md: -------------------------------------------------------------------------------- 1 | # 2.4 非排他锁 Semaphore :并发线程数限制 2 | 3 | ### 导读 4 | 5 | 在本章中,将学 Semaphore 和 SemaphoreSlim,两者都可以限制同时访问某一资源或资源池的线程数,实现并发时限制具体数量的线程进行并发操作。与 lock 不同的时,Semaphore 运行多个线程同时执行相同的区域代码,因此称为非排他锁。 6 | 7 | 在本章中,我们从案例入手,通过示例代码,慢慢深入了解。 8 | 9 | 10 | 11 | ## Semaphore 类 12 | 13 | 这里,先列出 Semaphore 类常用的 API。 14 | 15 | 其构造函数如下: 16 | 17 | | 构造函数 | 说明 | 18 | | ---------------------------------------- | ------------------------------------------------------------ | 19 | | Semaphore(Int32, Int32) | 初始化 Semaphore 类的新实例,并指定初始入口数和最大并发入口数。 | 20 | | Semaphore(Int32, Int32, String) | 初始化 Semaphore 类的新实例,并指定初始入口数和最大并发入口数,根据需要指定系统信号灯对象的名称。 | 21 | | Semaphore(Int32, Int32, String, Boolean) | 初始化 Semaphore 类的新实例,并指定初始入口数和最大并发入口数,还可以选择指定系统信号量对象的名称,以及指定一个变量来接收指示是否创建了新系统信号量的值。 | 22 | 23 | Semaphore 使用纯粹的内核时间(kernel-time)方式(等待时间很短),并且支持在不同的进程间同步线程(像Mutex)。 24 | 25 | 26 | 27 | Semaphore 常用方法如下: 28 | 29 | | 方法 | 说明 | 30 | | ---------------------------------- | ------------------------------------------------------------ | 31 | | Close() | 释放由当前 WaitHandle占用的所有资源。 | 32 | | OpenExisting(String) | 打开指定名称为信号量(如果已经存在)。 | 33 | | Release() | 退出信号量并返回前一个计数。 | 34 | | Release(Int32) | 以指定的次数退出信号量并返回前一个计数。 | 35 | | TryOpenExisting(String, Semaphore) | 打开指定名称为信号量(如果已经存在),并返回指示操作是否成功的值。 | 36 | | WaitOne() | 阻止当前线程,直到当前 WaitHandle 收到信号。 | 37 | | WaitOne(Int32) | 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 | 38 | | WaitOne(Int32, Boolean) | 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 | 39 | | WaitOne(TimeSpan) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。 | 40 | | WaitOne(TimeSpan, Boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。 | 41 | 42 | 43 | 44 | ### 示例 45 | 46 | 我们来直接写代码,这里使用 《原子操作 Interlocked》 中的示例,现在我们要求,采用多个线程执行计算,但是只允许最多三个线程同时执行运行。 47 | 48 | 使用 Semaphore ,有四个步骤: 49 | 50 | 1. new 实例化 Semaphore,并设置最大线程数、初始化时可进入线程数; 51 | 52 | 2. 使用 `.WaitOne();` 获取进入权限(在获得进入权限前,线程处于阻塞状态)。 53 | 54 | 3. 离开时使用 `Release()` 释放占用。 55 | 56 | 4. `Close()` 释放Semaphore 对象。 57 | 58 | 59 | 60 | 《原子操作 Interlocked》 中的示例改进如下: 61 | 62 | ```csharp 63 | class Program 64 | { 65 | // 求和 66 | private static int sum = 0; 67 | private static Semaphore _pool; 68 | 69 | // 判断十个线程是否结束了。 70 | private static int isComplete = 0; 71 | // 第一个程序 72 | static void Main(string[] args) 73 | { 74 | Console.WriteLine("执行程序"); 75 | 76 | // 设置允许最大三个线程进入资源池 77 | // 一开始设置为0,就是初始化时允许几个线程进入 78 | // 这里设置为0,后面按下按键时,可以放通三个线程 79 | _pool = new Semaphore(0, 3); 80 | for (int i = 0; i < 10; i++) 81 | { 82 | Thread thread = new Thread(new ParameterizedThreadStart(AddOne)); 83 | thread.Start(i + 1); 84 | } 85 | Console.ForegroundColor = ConsoleColor.Red; 86 | Console.WriteLine("任意按下键(不要按关机键),可以打开资源池"); 87 | Console.ForegroundColor = ConsoleColor.White; 88 | Console.ReadKey(); 89 | 90 | // 准许三个线程进入 91 | _pool.Release(3); 92 | 93 | // 这里没有任何意义,就单纯为了演示查看结果。 94 | // 等待所有线程完成任务 95 | while (true) 96 | { 97 | if (isComplete >= 10) 98 | break; 99 | Thread.Sleep(TimeSpan.FromSeconds(1)); 100 | } 101 | Console.WriteLine("sum = " + sum); 102 | 103 | // 释放池 104 | _pool.Close(); 105 | 106 | } 107 | 108 | public static void AddOne(object n) 109 | { 110 | Console.WriteLine($" 线程{(int)n}启动,进入队列"); 111 | // 进入队列等待 112 | _pool.WaitOne(); 113 | Console.WriteLine($"第{(int)n}个线程进入资源池"); 114 | // 进入资源池 115 | for (int i = 0; i < 10; i++) 116 | { 117 | Interlocked.Add(ref sum, 1); 118 | Thread.Sleep(TimeSpan.FromMilliseconds(500)); 119 | } 120 | // 解除占用的资源池 121 | _pool.Release(); 122 | isComplete += 1; 123 | Console.WriteLine($" 第{(int)n}个线程退出资源池"); 124 | } 125 | } 126 | ``` 127 | 128 | ![Semaphoregif](images/Semaphoregif.gif) 129 | 130 | 131 | 132 | 133 | 134 | ### 示例说明 135 | 136 | 实例化 Semaphore 使用了`new Semaphore(0,3); ` ,其构造函数原型为 137 | 138 | ```csharp 139 | public Semaphore(int initialCount, int maximumCount); 140 | ``` 141 | 142 | initialCount 表示一开始允许几个进程进入资源池,如果设置为0,所有线程都不能进入,要一直等资源池放通。 143 | 144 | maximumCount 表示最大允许几个线程进入资源池。 145 | 146 | `Release()` 表示退出信号量并返回前一个计数。这个计数指的是资源池还可以进入多少个线程。 147 | 148 | 可以看一下下面的示例: 149 | 150 | ```csharp 151 | private static Semaphore _pool; 152 | static void Main(string[] args) 153 | { 154 | _pool = new Semaphore(0, 5); 155 | _pool.Release(5); 156 | new Thread(AddOne).Start(); 157 | Thread.Sleep(TimeSpan.FromSeconds(10)); 158 | _pool.Close(); 159 | } 160 | 161 | public static void AddOne() 162 | { 163 | _pool.WaitOne(); 164 | Thread.Sleep(1000); 165 | int count = _pool.Release(); 166 | Console.WriteLine("在此线程退出资源池前,资源池还有多少线程可以进入?" + count); 167 | } 168 | ``` 169 | 170 | 171 | 172 | ### 信号量 173 | 174 | 前面我们学习到 Mutex,这个类是全局操作系统起作用的。我们从 Mutex 和 Semphore 中,也看到了 信号量这个东西,用于进程同步。 175 | 176 | 信号量分为两种类型:本地信号量和命名系统信号量。 177 | 178 | * 命名系统信号量在整个操作系统中均可见,可用于同步进程的活动。 179 | 180 | * 局部信号量仅存在于进程内。 181 | 182 | 当 name 为 null 或者为空时,Mutex 的信号量时局部信号量,否则 Mutex 的信号量是命名系统信号量。Semaphore 的话,也是两种方式都有。 183 | 184 | 如果使用接受名称的构造函数创建 Semaphor 对象,则该对象将与该名称的操作系统信号量关联。 185 | 186 | 两个构造函数: 187 | 188 | ```csharp 189 | Semaphore(Int32, Int32, String) 190 | Semaphore(Int32, Int32, String, Boolean) 191 | ``` 192 | 193 | 上面的构造函数可以创建多个表示同一命名系统信号量的 Semaphore 对象,并可以使用 OpenExisting 方法打开现有的已命名系统信号量。 194 | 195 | 我们上面使用的示例就是局部信号量,进程中引用本地 Semaphore 对象的所有线程都可以使用。 每个 Semaphore 对象都是单独的本地信号量。 196 | 197 | 可以利用 Semaphore 限制一个程序最多能够同时运行多少个。 198 | 199 | 200 | 201 | ## SemaphoreSlim类 202 | 203 | SemaphoreSlim 跟 Semaphore 有啥关系? 204 | 205 | 我看一下书再回答你。 206 | 207 | ![file](./images/image-1586681324216.png) 208 | 209 | 210 | 211 | 哦哦哦,微软文档说: 212 | 213 | SemaphoreSlim 表示对可同时访问资源或资源池的线程数加以限制的 Semaphore 的轻量替代。 214 | 215 | SemaphoreSlim 不使用信号量,不支持进程间同步,只能在进程内使用。 216 | 217 | 218 | 219 | 它有两个构造函数: 220 | 221 | | 构造函数 | 说明 | 222 | | --------------------------- | ------------------------------------------------------------ | 223 | | SemaphoreSlim(Int32) | 初始化 SemaphoreSlim 类的新实例,以指定可同时授予的请求的初始数量。 | 224 | | SemaphoreSlim(Int32, Int32) | 初始化 SemaphoreSlim 类的新实例,同时指定可同时授予的请求的初始数量和最大数量。 | 225 | 226 | 227 | 228 | ### 示例 229 | 230 | 我们改造一下前面 Semaphore 中的示例: 231 | 232 | ```csharp 233 | class Program 234 | { 235 | // 求和 236 | private static int sum = 0; 237 | private static SemaphoreSlim _pool; 238 | 239 | // 判断十个线程是否结束了。 240 | private static int isComplete = 0; 241 | static void Main(string[] args) 242 | { 243 | Console.WriteLine("执行程序"); 244 | 245 | // 设置允许最大三个线程进入资源池 246 | // 一开始设置为0,就是初始化时允许几个线程进入 247 | // 这里设置为0,后面按下按键时,可以放通三个线程 248 | _pool = new SemaphoreSlim(0, 3); 249 | for (int i = 0; i < 10; i++) 250 | { 251 | Thread thread = new Thread(new ParameterizedThreadStart(AddOne)); 252 | thread.Start(i + 1); 253 | } 254 | 255 | Console.WriteLine("任意按下键(不要按关机键),可以打开资源池"); 256 | Console.ReadKey(); 257 | // 258 | _pool.Release(3); 259 | 260 | // 这里没有任何意义,就单纯为了演示查看结果。 261 | // 等待所有线程完成任务 262 | while (true) 263 | { 264 | if (isComplete >= 10) 265 | break; 266 | Thread.Sleep(TimeSpan.FromSeconds(1)); 267 | } 268 | Console.WriteLine("sum = " + sum); 269 | // 释放池 270 | } 271 | 272 | public static void AddOne(object n) 273 | { 274 | Console.WriteLine($" 线程{(int)n}启动,进入队列"); 275 | // 进入队列等待 276 | _pool.Wait(); 277 | Console.WriteLine($"第{(int)n}个线程进入资源池"); 278 | // 进入资源池 279 | for (int i = 0; i < 10; i++) 280 | { 281 | Interlocked.Add(ref sum, 1); 282 | Thread.Sleep(TimeSpan.FromMilliseconds(200)); 283 | } 284 | // 解除占用的资源池 285 | _pool.Release(); 286 | isComplete += 1; 287 | Console.WriteLine($" 第{(int)n}个线程退出资源池"); 288 | } 289 | } 290 | ``` 291 | 292 | SemaphoreSlim 不需要 `Close()`。 293 | 294 | 两者在代码上的区别是就这么简单。 295 | 296 | 297 | 298 | 299 | 300 | ## 区别 301 | 302 | 如果使用下面的构造函数实例化 Semaphor(参数name不能为空),那么**创建的对象在整个操作系统内都有效**。 303 | 304 | ```csharp 305 | public Semaphore (int initialCount, int maximumCount, string name); 306 | ``` 307 | 308 | Semaphorslim 则只在进程内有效,SemaphoreSlim 是对 Semaphore 的简单封装。 309 | 310 | 311 | 312 | SemaphoreSlim 类不会对 `Wait`、`WaitAsync` 和 `Release` 方法的调用强制执行线程或任务标识。 313 | 314 | 而 Semaphor 类,会对此进行严格监控,如果对应调用数量不一致,会出现异常。 315 | 316 |

317 |

318 | 此外,如果使用 SemaphoreSlim(Int32 maximumCount) 构造函数来实例化 SemaphoreSlim 对象,获取其 CurrentCount 属性,其值可能会大于 maximumCount。 编程人员应负责确保调用一个 Wait 或 WaitAsync 方法,便调用一个 Release。 319 |
320 |

321 | 322 | 323 | 这就好像笔筒里面的笔,没有监控,使用这使用完毕后,都应该将笔放进去。如果原先有10支笔,每次使用不放进去,或者将别的地方的笔放进去,那么最后数量就不是10了。因此使用时需要注意捕获异常,合理释放锁。 324 | 325 | 326 | 327 | 328 | ![file](./images/image-1587217831610.png) -------------------------------------------------------------------------------- /3.task/9.task_scheduler.md: -------------------------------------------------------------------------------- 1 | # 3.9 自定义任务调度 2 | 3 | 4 | ```csharp 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | class Example 11 | { 12 | static void Main() 13 | { 14 | // Create a scheduler that uses two threads. 15 | LimitedConcurrencyLevelTaskScheduler lcts = new LimitedConcurrencyLevelTaskScheduler(2); 16 | List tasks = new List(); 17 | 18 | // Create a TaskFactory and pass it our custom scheduler. 19 | TaskFactory factory = new TaskFactory(lcts); 20 | CancellationTokenSource cts = new CancellationTokenSource(); 21 | 22 | // Use our factory to run a set of tasks. 23 | Object lockObj = new Object(); 24 | int outputItem = 0; 25 | 26 | for (int tCtr = 0; tCtr <= 4; tCtr++) 27 | { 28 | int iteration = tCtr; 29 | Task t = factory.StartNew(() => { 30 | for (int i = 0; i < 1000; i++) 31 | { 32 | lock (lockObj) 33 | { 34 | Console.Write("{0} in task t-{1} on thread {2} ", 35 | i, iteration, Thread.CurrentThread.ManagedThreadId); 36 | outputItem++; 37 | if (outputItem % 3 == 0) 38 | Console.WriteLine(); 39 | } 40 | } 41 | }, cts.Token); 42 | tasks.Add(t); 43 | } 44 | // Use it to run a second set of tasks. 45 | for (int tCtr = 0; tCtr <= 4; tCtr++) 46 | { 47 | int iteration = tCtr; 48 | Task t1 = factory.StartNew(() => { 49 | for (int outer = 0; outer <= 10; outer++) 50 | { 51 | for (int i = 0x21; i <= 0x7E; i++) 52 | { 53 | lock (lockObj) 54 | { 55 | Console.Write("'{0}' in task t1-{1} on thread {2} ", 56 | Convert.ToChar(i), iteration, Thread.CurrentThread.ManagedThreadId); 57 | outputItem++; 58 | if (outputItem % 3 == 0) 59 | Console.WriteLine(); 60 | } 61 | } 62 | } 63 | }, cts.Token); 64 | tasks.Add(t1); 65 | } 66 | 67 | // Wait for the tasks to complete before displaying a completion message. 68 | Task.WaitAll(tasks.ToArray()); 69 | cts.Dispose(); 70 | Console.WriteLine("\n\nSuccessful completion."); 71 | } 72 | } 73 | 74 | // Provides a task scheduler that ensures a maximum concurrency level while 75 | // running on top of the thread pool. 76 | public class LimitedConcurrencyLevelTaskScheduler : TaskScheduler 77 | { 78 | // Indicates whether the current thread is processing work items. 79 | [ThreadStatic] 80 | private static bool _currentThreadIsProcessingItems; 81 | 82 | // The list of tasks to be executed 83 | private readonly LinkedList _tasks = new LinkedList(); // protected by lock(_tasks) 84 | 85 | // The maximum concurrency level allowed by this scheduler. 86 | private readonly int _maxDegreeOfParallelism; 87 | 88 | // Indicates whether the scheduler is currently processing work items. 89 | private int _delegatesQueuedOrRunning = 0; 90 | 91 | // Creates a new instance with the specified degree of parallelism. 92 | public LimitedConcurrencyLevelTaskScheduler(int maxDegreeOfParallelism) 93 | { 94 | if (maxDegreeOfParallelism < 1) throw new ArgumentOutOfRangeException("maxDegreeOfParallelism"); 95 | _maxDegreeOfParallelism = maxDegreeOfParallelism; 96 | } 97 | 98 | // Queues a task to the scheduler. 99 | protected sealed override void QueueTask(Task task) 100 | { 101 | // Add the task to the list of tasks to be processed. If there aren't enough 102 | // delegates currently queued or running to process tasks, schedule another. 103 | lock (_tasks) 104 | { 105 | _tasks.AddLast(task); 106 | if (_delegatesQueuedOrRunning < _maxDegreeOfParallelism) 107 | { 108 | ++_delegatesQueuedOrRunning; 109 | NotifyThreadPoolOfPendingWork(); 110 | } 111 | } 112 | } 113 | 114 | // Inform the ThreadPool that there's work to be executed for this scheduler. 115 | private void NotifyThreadPoolOfPendingWork() 116 | { 117 | ThreadPool.UnsafeQueueUserWorkItem(_ => 118 | { 119 | // Note that the current thread is now processing work items. 120 | // This is necessary to enable inlining of tasks into this thread. 121 | _currentThreadIsProcessingItems = true; 122 | try 123 | { 124 | // Process all available items in the queue. 125 | while (true) 126 | { 127 | Task item; 128 | lock (_tasks) 129 | { 130 | // When there are no more items to be processed, 131 | // note that we're done processing, and get out. 132 | if (_tasks.Count == 0) 133 | { 134 | --_delegatesQueuedOrRunning; 135 | break; 136 | } 137 | 138 | // Get the next item from the queue 139 | item = _tasks.First.Value; 140 | _tasks.RemoveFirst(); 141 | } 142 | 143 | // Execute the task we pulled out of the queue 144 | base.TryExecuteTask(item); 145 | } 146 | } 147 | // We're done processing items on the current thread 148 | finally { _currentThreadIsProcessingItems = false; } 149 | }, null); 150 | } 151 | 152 | // Attempts to execute the specified task on the current thread. 153 | protected sealed override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued) 154 | { 155 | // If this thread isn't already processing a task, we don't support inlining 156 | if (!_currentThreadIsProcessingItems) return false; 157 | 158 | // If the task was previously queued, remove it from the queue 159 | if (taskWasPreviouslyQueued) 160 | // Try to run the task. 161 | if (TryDequeue(task)) 162 | return base.TryExecuteTask(task); 163 | else 164 | return false; 165 | else 166 | return base.TryExecuteTask(task); 167 | } 168 | 169 | // Attempt to remove a previously scheduled task from the scheduler. 170 | protected sealed override bool TryDequeue(Task task) 171 | { 172 | lock (_tasks) return _tasks.Remove(task); 173 | } 174 | 175 | // Gets the maximum concurrency level supported by this scheduler. 176 | public sealed override int MaximumConcurrencyLevel { get { return _maxDegreeOfParallelism; } } 177 | 178 | // Gets an enumerable of the tasks currently scheduled on this scheduler. 179 | protected sealed override IEnumerable GetScheduledTasks() 180 | { 181 | bool lockTaken = false; 182 | try 183 | { 184 | Monitor.TryEnter(_tasks, ref lockTaken); 185 | if (lockTaken) return _tasks; 186 | else throw new NotSupportedException(); 187 | } 188 | finally 189 | { 190 | if (lockTaken) Monitor.Exit(_tasks); 191 | } 192 | } 193 | } 194 | // The following is a portion of the output from a single run of the example: 195 | // 'T' in task t1-4 on thread 3 'U' in task t1-4 on thread 3 'V' in task t1-4 on thread 3 196 | // 'W' in task t1-4 on thread 3 'X' in task t1-4 on thread 3 'Y' in task t1-4 on thread 3 197 | // 'Z' in task t1-4 on thread 3 '[' in task t1-4 on thread 3 '\' in task t1-4 on thread 3 198 | // ']' in task t1-4 on thread 3 '^' in task t1-4 on thread 3 '_' in task t1-4 on thread 3 199 | // '`' in task t1-4 on thread 3 'a' in task t1-4 on thread 3 'b' in task t1-4 on thread 3 200 | // 'c' in task t1-4 on thread 3 'd' in task t1-4 on thread 3 'e' in task t1-4 on thread 3 201 | // 'f' in task t1-4 on thread 3 'g' in task t1-4 on thread 3 'h' in task t1-4 on thread 3 202 | // 'i' in task t1-4 on thread 3 'j' in task t1-4 on thread 3 'k' in task t1-4 on thread 3 203 | // 'l' in task t1-4 on thread 3 'm' in task t1-4 on thread 3 'n' in task t1-4 on thread 3 204 | // 'o' in task t1-4 on thread 3 'p' in task t1-4 on thread 3 ']' in task t1-2 on thread 4 205 | // '^' in task t1-2 on thread 4 '_' in task t1-2 on thread 4 '`' in task t1-2 on thread 4 206 | // 'a' in task t1-2 on thread 4 'b' in task t1-2 on thread 4 'c' in task t1-2 on thread 4 207 | // 'd' in task t1-2 on thread 4 'e' in task t1-2 on thread 4 'f' in task t1-2 on thread 4 208 | // 'g' in task t1-2 on thread 4 'h' in task t1-2 on thread 4 'i' in task t1-2 on thread 4 209 | // 'j' in task t1-2 on thread 4 'k' in task t1-2 on thread 4 'l' in task t1-2 on thread 4 210 | // 'm' in task t1-2 on thread 4 'n' in task t1-2 on thread 4 'o' in task t1-2 on thread 4 211 | // 'p' in task t1-2 on thread 4 'q' in task t1-2 on thread 4 'r' in task t1-2 on thread 4 212 | // 's' in task t1-2 on thread 4 't' in task t1-2 on thread 4 'u' in task t1-2 on thread 4 213 | // 'v' in task t1-2 on thread 4 'w' in task t1-2 on thread 4 'x' in task t1-2 on thread 4 214 | // 'y' in task t1-2 on thread 4 'z' in task t1-2 on thread 4 '{' in task t1-2 on thread 4 215 | // '|' in task t1-2 on thread 4 '}' in task t1-2 on thread 4 '~' in task t1-2 on thread 4 216 | // 'q' in task t1-4 on thread 3 'r' in task t1-4 on thread 3 's' in task t1-4 on thread 3 217 | // 't' in task t1-4 on thread 3 'u' in task t1-4 on thread 3 'v' in task t1-4 on thread 3 218 | // 'w' in task t1-4 on thread 3 'x' in task t1-4 on thread 3 'y' in task t1-4 on thread 3 219 | // 'z' in task t1-4 on thread 3 '{' in task t1-4 on thread 3 '|' in task t1-4 on thread 3 220 | ``` -------------------------------------------------------------------------------- /2.thread_sync/10.spinwait.md: -------------------------------------------------------------------------------- 1 | # 2.10 自旋 2 | 3 | 自旋和阻塞的区别自旋与阻塞有一些细微的差别。首先,非常短暂的自旋在条件可以很快得到满足的场景(例如几微秒)下是非常高效的,因为它避免了上下文切换带来的延迟和开销。 4 | 5 | .NET Core提供了一些特殊的辅助方法和类来进行这一操作,请参见http://albahari.com/threading/的 SpinLock and SpinWait。 6 | 7 | 其次,阻塞并非零开销。这是因为每一个线程在存活时会占用 1 MB左右的内存,并对CLR和操作系统带来持续性的管理开销。因此,阻塞可能会给繁重的I/O密集型程序(例如要处理成百上千的并发操作)带来麻烦。这些程序更适于使用回调的方式,在等待时完全解除这些线程。我们将在后面讨论异步模式的时候介绍这种方法。 8 | 9 | 前面我们学习了很多用于线程管理的 类型,也学习了多种线程同步的使用方法,这一篇主要讲述线程等待相关的内容。 10 | 11 | 前面已经探究了创建线程的创建姿势和各种锁的使用,也学习了很多类型,也使用到了很多种等待方法,例如 `Thread.Sleep()`、`Thread.SpinWait();`、`{某种锁}.WaitOne()` 等。 12 | 13 | 这些等待会影响代码的算法逻辑和程序的性能,也有可能会造成死锁,在本篇我们将会慢慢探究线程中等待。 14 | 15 | 16 | 17 | ### volatile 关键字 18 | 19 | ![image-20220327132544351](images/image-20220327132544351.png) 20 | 21 | 22 | 23 | `volatile` 关键字指示一个字段可以由多个同时执行的线程修改。在 2.1 章的原子操作已经介绍过 volatile ,这里就不再赘述。 24 | 25 | volatile 的作用在于读,保证了观察的顺序和写入的顺序一致,每次读取的都是最新的一个值,不会干扰写操作。 26 | 27 | 详情请点击:[https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/volatile) 28 | 29 | 其原理解释:[https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/](https://theburningmonk.com/2010/03/threading-understanding-the-volatile-modifier-in-csharp/) 30 | 31 | ![file](./images/image-1587871331160.png) 32 | 33 | volatile 在一定意义上也可以完成线程同步的功能。 34 | 35 | ```csharp 36 | private static volatile bool IsStop; 37 | static void Main(string[] args) 38 | { 39 | while (!IsStop) 40 | { 41 | } 42 | } 43 | ``` 44 | 45 | 46 | 47 | 48 | 49 | ### 三种常用等待 50 | 51 | 这三种等待分别是: 52 | 53 | ```csharp 54 | Thread.Sleep(); 55 | ``` 56 | 57 | ```csharp 58 | Thread.SpinWait(); 59 | ``` 60 | 61 | ```csharp 62 | Task.Delay(); 63 | ``` 64 | 65 | `Thread.Sleep();` 会阻塞线程,使得线程交出时间片,然后处于休眠状态,直至被重新唤醒;适合用于长时间的等待; 66 | 67 |
68 | 69 | `Thread.SpinWait();` 使用了自旋等待,等待过程中会进行一些的运算,线程不会休眠,用于微小的时间等待;长时间等待会影响性能; 70 | 71 |
72 | 73 | `Task.Delay();` 用于异步中的等待,异步的文章后面才写,这里先不理会; 74 | 75 |
76 | 77 | 这里我们还需要继续 SpinWait 和 SpinLock 这两个类型,最后再进行总结对照。 78 | 79 | 80 | 81 | ### 再说自旋和阻塞 82 | 83 | 前面我们学习过自旋和阻塞的区别,这里再来撸清楚一下。 84 | 85 | 线程等待有内核模式(Kernel Mode)和用户模式(User Model)。 86 | 87 | 因为只有操作系统才能控制线程的生命周期,因此使用 `Thread.Sleep()` 等方式阻塞线程,发生上下文切换,此种等待称为内核模式。 88 | 89 | 用户模式使线程等待,并不需要线程切换上下文,而是让线程通过执行一些无意义的运算,实现等待,也称为自旋。 90 | 91 | 我们来对比一下 `Thread.Sleep(1)` 和 `Thread.SpinWait(1)` 占用的时间。 92 | 93 | ```csharp 94 | static void Main(string[] args) 95 | { 96 | Stopwatch stopwatch = new Stopwatch(); 97 | stopwatch.Start(); 98 | Thread.Sleep(1); 99 | Console.WriteLine(stopwatch.Elapsed.ToString()); 100 | Console.ReadKey(); 101 | } 102 | ``` 103 | 104 | ```csharp 105 | static void Main(string[] args) 106 | { 107 | Stopwatch stopwatch = new Stopwatch(); 108 | stopwatch.Start(); 109 | Thread.SpinWait(1); 110 | Console.WriteLine(stopwatch.Elapsed.ToString()); 111 | Console.ReadKey(); 112 | } 113 | ``` 114 | 115 | 输出结果: 116 | 117 | ``` 118 | 00:00:00.0014747 119 | 00:00:00.0000214 120 | ``` 121 | 122 | 可以看到,自旋一次消耗的时间远远低于 `1ms`,并且 ` Thread.Sleep` 会出现上下文切换,而 `Thread.SpinWait` 不会。`Thread.SpinWait` 适合等待短暂的任务,实现线程同步。 123 | 124 | 125 | 126 | ## SpinWait 结构 127 | 128 | 微软文档定义:为基于自旋的等待提供支持。 129 | 130 |

131 |

132 | SpinWait 是结构体;Thread.SpinWait() 的原理就是 SpinWait 。
如果你想了解 Thread.SpinWait() 是怎么实现的,可以参考 https://www.tabsoverspaces.com/233735-how-is-thread-spinwait-actually-implemented 133 |
134 |

135 | 136 | 线程阻塞是会耗费上下文切换的,对于过短的线程等待,这种切换的代价会比较昂贵的。在我们前面的示例中,大量使用了 `Thread.Sleep()` 和各种类型的等待方法,这其实是不合理的。 137 | 138 | SpinWait 则提供了更好的选择。 139 | 140 | 141 | 142 | ### 属性和方法 143 | 144 | 老规矩,先来看一下 SpinWait 常用的属性和方法。 145 | 146 | 属性: 147 | 148 | | 属性 | 说明 | 149 | | ----------------- | ------------------------------------------------------------ | 150 | | Count | 获取已对此实例调用 SpinOnce() 的次数。 | 151 | | NextSpinWillYield | 获取对 SpinOnce() 的下一次调用是否将产生处理器,同时触发强制上下文切换。 | 152 | 153 | 方法: 154 | 155 | | 方法 | 说明 | 156 | | ------------------------- | -------------------------------------------------------- | 157 | | Reset() | 重置自旋计数器。 | 158 | | SpinOnce() | 执行单一自旋。 | 159 | | SpinOnce(Int32) | 执行单一自旋,并在达到最小旋转计数后调用 Sleep(Int32) 。 | 160 | | SpinUntil(Func) | 在指定条件得到满足之前自旋。 | 161 | | SpinUntil(Func, Int32) | 在指定条件得到满足或指定超时过期之前自旋。 | 162 | | SpinUntil(Func, TimeSpan) | 在指定条件得到满足或指定超时过期之前自旋。 | 163 | 164 | 165 | 166 | ### 自旋等待 167 | 168 | 下面来实现一个让当前线程等待其它线程完成任务的功能。 169 | 170 | 其功能是开辟一个线程对 sum 进行 `+1`,当新的线程完成运算后,主线程才能继续运行。 171 | 172 | ```csharp 173 | class Program 174 | { 175 | static void Main(string[] args) 176 | { 177 | new Thread(DoWork).Start(); 178 | 179 | // 等待上面的线程完成工作 180 | MySleep(); 181 | 182 | Console.WriteLine("sum = " + sum); 183 | Console.ReadKey(); 184 | } 185 | 186 | private static int sum = 0; 187 | private static void DoWork() 188 | { 189 | for (int i = 0; i < 1000_0000; i++) 190 | { 191 | sum++; 192 | } 193 | isCompleted = true; 194 | } 195 | 196 | // 自定义等待等待 197 | private static bool isCompleted = false; 198 | private static void MySleep() 199 | { 200 | int i = 0; 201 | while (!isCompleted) 202 | { 203 | i++; 204 | } 205 | } 206 | } 207 | ``` 208 | 209 | 210 | 211 | 我们改进上面的示例,修改 MySleep 方法,改成: 212 | 213 | ```csharp 214 | private static bool isCompleted = false; 215 | private static void MySleep() 216 | { 217 | SpinWait wait = new SpinWait(); 218 | while (!isCompleted) 219 | { 220 | wait.SpinOnce(); 221 | } 222 | } 223 | ``` 224 | 225 | 或者改成 226 | 227 | ```csharp 228 | private static bool isCompleted = false; 229 | private static void MySleep() 230 | { 231 | SpinWait.SpinUntil(() => isCompleted); 232 | } 233 | ``` 234 | 235 | 236 | 237 | ## SpinLock 结构 238 | 239 | 微软文档:提供一个相互排斥锁基元,在该基元中,尝试获取锁的线程将在重复检查的循环中等待,直至该锁变为可用为止。 240 | 241 | SpinLock 称为自旋锁,适合用在频繁争用而且等待时间较短的场景。主要特征是避免了阻塞,不出现昂贵的上下文切换。 242 | 243 | 笔者水平有限,关于 SpinLock ,可以参考 [https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/](https://www.c-sharpcorner.com/UploadFile/1d42da/spinlock-class-in-threading-C-Sharp/) 244 | 245 | 另外,还记得 Monitor 嘛?SpinLock 跟 Monitor 比较像噢~[https://www.cnblogs.com/whuanle/p/12722853.html#2monitor](https://www.cnblogs.com/whuanle/p/12722853.html#2monitor) 246 | 247 | 在读写锁中,我们介绍了 ReaderWriterLock 和 ReaderWriterLockSlim ,而 ReaderWriterLockSlim 内部依赖于 SpinLock,并且比 ReaderWriterLock 快了三倍。 248 | 249 | 250 | 251 | ### 属性和方法 252 | 253 | SpinLock 常用属性和方法如下: 254 | 255 | 属性: 256 | 257 | | 属性 | 说明 | 258 | | ---------------------------- | ---------------------------------------- | 259 | | IsHeld | 获取锁当前是否已由任何线程占用。 | 260 | | IsHeldByCurrentThread | 获取锁是否已由当前线程占用。 | 261 | | IsThreadOwnerTrackingEnabled | 获取是否已为此实例启用了线程所有权跟踪。 | 262 | 263 | 方法: 264 | 265 | | 方法 | 说明 | 266 | | --------------------------- | ------------------------------------------------------------ | 267 | | Enter(Boolean) | 采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 `lockTaken` 以确定是否已获取锁。 | 268 | | Exit() | 释放锁。 | 269 | | Exit(Boolean) | 释放锁。 | 270 | | TryEnter(Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 `lockTaken` 以确定是否已获取锁。 | 271 | | TryEnter(Int32, Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 `lockTaken` 以确定是否已获取锁。 | 272 | | TryEnter(TimeSpan, Boolean) | 尝试采用可靠的方式获取锁,这样,即使在方法调用中发生异常的情况下,都能采用可靠的方式检查 `lockTaken` 以确定是否已获取锁。 | 273 | 274 | 275 | 276 | ### 示例 277 | 278 | SpinLock 的模板如下: 279 | 280 | ```csharp 281 | private static void DoWork() 282 | { 283 | SpinLock spinLock = new SpinLock(); 284 | bool isGetLock = false; // 是否已获得了锁 285 | try 286 | { 287 | spinLock.Enter(ref isGetLock); 288 | // 运算 289 | } 290 | finally 291 | { 292 | if (isGetLock) 293 | spinLock.Exit(); 294 | } 295 | } 296 | ``` 297 | 298 | 这里就不写场景示例了。 299 | 300 | 需要注意的是, SpinLock 实例不能共享,也不能重复使用。 301 | 302 | 303 | 304 | ### 等待时间对比 305 | 306 | 大佬的文章,.NET 中的多种锁性能测试数据:[http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/](http://kejser.org/synchronisation-in-net-part-3-spinlocks-and-interlocks/) 307 | 308 | 这里我们简单测试一下阻塞和自旋的等待时间测试对比。 309 | 310 | 我们经常说,`Thread.Sleep()` 会发生上下文切换,出现比较大的性能损失,然后就是时间上的差异具体有多大呢?我们来测试一下。(以下运算都是在 Debug 下测试) 311 | 312 | 测试 `Thread.Sleep(1)`: 313 | 314 | ```csharp 315 | private static void DoWork() 316 | { 317 | Stopwatch watch = new Stopwatch(); 318 | watch.Start(); 319 | for (int i = 0; i < 1_0000; i++) 320 | { 321 | Thread.Sleep(1); 322 | } 323 | watch.Stop(); 324 | Console.WriteLine(watch.ElapsedMilliseconds); 325 | } 326 | ``` 327 | 328 | 笔者机器测试,结果大约 20018。`Thread.Sleep(1)` 减去等待的时间 10000 毫秒,那么进行 10000 次上下文切换需要花费 10000 毫秒,约每次 1 毫秒。 329 | 330 | 上面示例改成: 331 | 332 | ```csharp 333 | for (int i = 0; i < 1_0000; i++) 334 | { 335 | Thread.Sleep(2); 336 | } 337 | ``` 338 | 339 | 运算,发现结果为 30013,也说明了上下文切换,大约需要一毫秒。 340 | 341 | 342 | 343 | 改成 `Thread.SpinWait(1000)`: 344 | 345 | ```csharp 346 | for (int i = 0; i < 100_0000; i++) 347 | { 348 | Thread.SpinWait(1000); 349 | } 350 | ``` 351 | 352 | 结果为 28876,说明自旋 1000 次,大约需要 0.03 毫秒。 353 | 354 | 如果需要等待的时间很短,那就最好使用 ` Thread.SpinWait`,让线程继续占用短时间的 CPU 什么也不做,避免出现线程上下文切换。 355 | 356 | 357 | 358 | ### 自旋和休眠 359 | 360 | 当线程处于进入休眠状态或解除休眠状态时,会发生上下文切换,这就带来了昂贵的消耗。 361 | 362 | 而线程不断运行,就会消耗 CPU 时间,占用 CPU 资源。 363 | 364 | 365 | 366 | 对于过短的等待,应该使用自旋(spin)方法,避免发生上下文切换;过长的等待应该使线程休眠,避免占用大量 CPU 时间。 367 | 368 | 我们可以使用最为熟知的 `Sleep()` 方法休眠线程。有很多同步线程的类型,也使用了休眠手段等待线程(已经写好草稿啦)。 369 | 370 | 371 | 372 | 自旋的意思是,没事找事做。 373 | 374 | 例如: 375 | 376 | ```csharp 377 | public static void Test(int n) 378 | { 379 | int num = 0; 380 | for (int i=0;i 400 |
401 | SpinWait 无法使你准确控制等待时间,主要是使用一些锁时用到,例如 Monitor.Enter。 402 |
403 |

404 | -------------------------------------------------------------------------------- /2.thread_sync/3.mutex.md: -------------------------------------------------------------------------------- 1 | # 2.3 Mutex 锁 2 | 3 | 4 | 5 | ### 导读 6 | 7 | Mutex 中文为互斥,Mutex 类叫做互斥锁。它还可用于进程间同步的同步基元。Mutex 跟 lock 相似,但是 Mutex 支持多个进程,Mutex 大约比 lock 慢 20 倍。 8 | 9 | 10 | 互斥锁(Mutex),用于多线程中防止两条线程同时对一个公共资源进行读写的机制,Mutex 只能在获得锁的线程中释放锁。 11 | 12 | 13 | 14 | Windows 操作系统中,Mutex 同步对象有两个状态: 15 | 16 | * signaled:未被任何对象拥有; 17 | 18 | * nonsignaled:被一个线程拥有; 19 | 20 | 21 | 22 | ## Mutex 锁 23 | 24 | ### 构造函数和方法 25 | 26 | Mutex 类其构造函数如下: 27 | 28 | | 构造函数 | 说明 | 29 | | ------------------------------- | ------------------------------------------------------------ | 30 | | Mutex() | 使用默认属性初始化 Mutex类的新实例。 | 31 | | Mutex(Boolean) | 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权)初始化 Mutex 类的新实例。 | 32 | | Mutex(Boolean, String) | 使用 Boolean 值(指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称)初始化 Mutex 类的新实例。 | 33 | | Mutex(Boolean, String, Boolean) | 使用可指示调用线程是否应具有互斥体的初始所有权以及字符串是否为互斥体的名称的 Boolean 值和当线程返回时可指示调用线程是否已赋予互斥体的初始所有权的 Boolean 值初始化 Mutex 类的新实例。 | 34 | 35 | Mutex 对于进程同步有所帮助,例如其应用场景主要是控制系统只能运行一个此程序的实例。 36 | 37 |

38 | 39 | Mutex 构造函数中的 String类型参数 叫做互斥量而互斥量是全局的操作系统对象。 40 |
41 | Mutex 只要考虑实现进程间的同步,它会耗费比较多的资源,进程内请考虑 Monitor/lock。 42 |     43 |

44 | 45 | 46 | 47 | Mutex 的常用方法如下: 48 | 49 | | 方法 | 说明 | 50 | | ------------------------------ | ------------------------------------------------------------ | 51 | | Close() | 释放由当前 WaitHandle 占用的所有资源。 | 52 | | Dispose() | 释放由 WaitHandle 类的当前实例占用的所有资源。 | 53 | | OpenExisting(String) | 打开指定的已命名的互斥体(如果已经存在)。 | 54 | | ReleaseMutex() | 释放 Mutex一次。 | 55 | | TryOpenExisting(String, Mutex) | 打开指定的已命名的互斥体(如果已经存在),并返回指示操作是否成功的值。 | 56 | | WaitOne() | 阻止当前线程,直到当前 WaitHandle 收到信号。 | 57 | | WaitOne(Int32) | 阻止当前线程,直到当前 WaitHandle 收到信号,同时使用 32 位带符号整数指定时间间隔(以毫秒为单位)。 | 58 | | WaitOne(Int32, Boolean) | 阻止当前线程,直到当前的 WaitHandle 收到信号为止,同时使用 32 位带符号整数指定时间间隔,并指定是否在等待之前退出同步域。 | 59 | | WaitOne(TimeSpan) | 阻止当前线程,直到当前实例收到信号,同时使用 TimeSpan 指定时间间隔。 | 60 | | WaitOne(TimeSpan, Boolean) | 阻止当前线程,直到当前实例收到信号为止,同时使用 TimeSpan 指定时间间隔,并指定是否在等待之前退出同步域。 | 61 | 62 | 关于 Mutex 类,我们可以先通过几个示例去了解它。 63 | 64 | 65 | 66 | ### 系统只能运行一个程序的实例 67 | 68 | 下面是一个示例,用于控制系统只能运行一个此程序的实例,不允许同时启动多次。 69 | 70 | ```csharp 71 | class Program 72 | { 73 | // 第一个程序 74 | const string name = "www.whuanle.cn"; 75 | private static Mutex m; 76 | static void Main(string[] args) 77 | { 78 | // 本程序是否是 Mutex 的拥有者 79 | bool firstInstance; 80 | m = new Mutex(false,name,out firstInstance); 81 | if (!firstInstance) 82 | { 83 | Console.WriteLine("程序已在运行!按下回车键退出!"); 84 | Console.ReadKey(); 85 | return; 86 | } 87 | Console.WriteLine("程序已经启动"); 88 | Console.WriteLine("按下回车键退出运行"); 89 | Console.ReadKey(); 90 | m.ReleaseMutex(); 91 | m.Close(); 92 | return; 93 | } 94 | } 95 | ``` 96 | 97 | 上面的代码中,有些地方前面没有讲,没关系,我们运行一下生成的程序先。 98 | 99 | 100 | 101 | ![](./images/Mutex1.gif) 102 | 103 | 104 | 105 | ### 解释一下上面的示例 106 | 107 | Mutex 的工作原理: 108 | 109 | 当两个或两个以上的线程同时访问共享资源时,操作系统需要一个同步机制来确保每次只有一个线程使用资源。 110 | 111 |

112 |      113 | Mutex 是一种同步基元,Mutex 仅向一个线程授予独占访问共享资源的权限。这个权限依据就是 互斥体,当一个线程获取到互斥体后,其它线程也在试图获取互斥体时,就会被挂起(阻塞),直到第一个线程释放互斥体。 114 |      115 |

116 | 117 | 118 | 119 | 对应我们上一个代码示例中,实例化 Mutex 类的构造函数如下: 120 | 121 | ```csharp 122 | m = new Mutex(false,name,out firstInstance); 123 | ``` 124 | 125 | 其构造函数原型如下: 126 | 127 | ```csharp 128 | public Mutex (bool initiallyOwned, string name, out bool createdNew); 129 | ``` 130 | 131 |
132 | 133 | 前面我们提出过,Mutex 对象有两种状态,signaled 和 nonsignaled。 134 | 135 | 通过 new 来实例化 Mutex 类,会检查系统中此互斥量 name 是否已经被使用,如果没有被使用,则会创建 name 互斥量并且此线程拥有此互斥量的使用权;此时 `createdNew == true`。 136 | 137 | 那么 initiallyOwned ,它的作用是是否允许线程是否能够获取到此互斥量的初始化所有权。因为我们希望只有一个程序能够在后台运行,因此我们要设置为 false。 138 | 139 | 140 | 141 | 驱动开发中关于Mutex :[https://docs.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-mutex-objects](https://docs.microsoft.com/zh-cn/windows-hardware/drivers/kernel/introduction-to-mutex-objects) 142 | 143 | 144 | 145 | 对了, Mutex 的 参数中,name 是非常有讲究的。 146 | 147 | 在运行终端服务的服务器上,命名系统 mutex 可以有两个级别的可见性。 148 | 149 | * 如果其名称以前缀 "Global\" 开头,则 mutex 在所有终端服务器会话中可见。 150 | 151 | * 如果其名称以前缀 "Local\" 开头,则 mutex 仅在创建它的终端服务器会话中可见。 在这种情况下,可以在服务器上的其他每个终端服务器会话中存在具有相同名称的单独 mutex。 152 | 153 | 如果在创建已命名的 mutex 时未指定前缀,则采用前缀 "Local\"。 在终端服务器会话中,两个互斥体的名称只是它们的前缀不同,它们都是对终端服务器会话中的所有进程都可见。 154 | 155 | 也就是说,前缀名称 "Global\" 和 "Local\" 描述互斥体名称相对于终端服务器会话的作用域,而不是相对于进程。 156 | 157 | 请参考: 158 | 159 | https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex?view=netcore-3.1#methods 160 | 161 | https://www.cnblogs.com/suntp/p/8258488.html 162 | 163 | 164 | 165 | ### 接替运行 166 | 167 | 这里要实现,当同时点击一个程序时,只能有一个实例A可以运行,其它实例进入等待队列,等待A运行完毕后,然后继续运行队列中的下一个实例。 168 | 169 | 我们将每个程序比作一个人,模拟一个厕所坑位,每次只能有一个人上厕所,其他人需要排队等候。 170 | 171 | 使用 `WaitOne()` 方法来等待别的进程释放互斥量,即模拟排队;`ReleaseMutex()` 方法解除对坑位的占用。 172 | 173 | ```csharp 174 | class Program 175 | { 176 | // 第一个程序 177 | const string name = "www.whuanle.cn"; 178 | private static Mutex m; 179 | static void Main(string[] args) 180 | { 181 | // wc 还有没有位置 182 | bool firstInstance; 183 | m = new Mutex(true,name,out firstInstance); 184 | 185 | // 已经有人在上wc 186 | if (!firstInstance) 187 | { 188 | // 等待运行的实例退出,此进程才能运行。 189 | Console.WriteLine("排队等待"); 190 | m.WaitOne(); 191 | GoWC(); 192 | return; 193 | } 194 | GoWC(); 195 | 196 | return; 197 | } 198 | 199 | private static void GoWC() 200 | { 201 | Console.WriteLine(" 开始上wc"); 202 | Thread.Sleep(1000); 203 | Console.WriteLine(" 开门"); 204 | Thread.Sleep(1000); 205 | Console.WriteLine(" 关门"); 206 | Thread.Sleep(1000); 207 | Console.WriteLine(" xxx"); 208 | Thread.Sleep(1000); 209 | Console.WriteLine(" 开门"); 210 | Thread.Sleep(1000); 211 | Console.WriteLine(" 离开wc"); 212 | m.ReleaseMutex(); 213 | Thread.Sleep(1000); 214 | Console.WriteLine(" 洗手"); 215 | } 216 | } 217 | ``` 218 | 219 | 220 | 221 | ![](./images/Mutex2.gif) 222 | 223 | 此时,我们使用了 224 | 225 | ```csharp 226 | m = new Mutex(true,name,out firstInstance); 227 | ``` 228 | 229 | 一个程序结束后,要允许其它线程能够创建 Mutex 对象获取互斥量,需要将构造函数的第一个参数设置为 true。 230 | 231 | 你也可以改成 false,看看会报什么异常。 232 | 233 | 你可以使用 `WaitOne(Int32)` 来设置等待时间,单位是毫秒,超过这个时间就不排队了,去别的地方上厕所。 234 | 235 | 236 | 237 | 为了避免出现问题,请考虑在 finally 块中执行 `m.ReleaseMutex()`。 238 | 239 | 240 | 241 | ### 进程同步示例 242 | 243 | 这里我们实现一个这样的场景: 244 | 245 | 父进程 Parent 启动子进程 Children ,等待子进程 Children 执行完毕,子进程退出,父进程退出。 246 | 247 | 新建一个 .NET Core 控制台项目,名称为 Children,其 Progarm 中的代码如下 248 | 249 | ```csharp 250 | using System; 251 | using System.Threading; 252 | 253 | namespace Children 254 | { 255 | class Program 256 | { 257 | const string name = "进程同步示例"; 258 | private static Mutex m; 259 | static void Main(string[] args) 260 | { 261 | Console.WriteLine("子进程被启动..."); 262 | bool firstInstance; 263 | 264 | // 子进程创建互斥体 265 | m = new Mutex(true, name, out firstInstance); 266 | 267 | // 按照我们设计的程序,创建一定是成功的 268 | if (firstInstance) 269 | { 270 | Console.WriteLine("子线程执行任务"); 271 | DoWork(); 272 | Console.WriteLine("子线程任务完成"); 273 | 274 | // 释放互斥体 275 | m.ReleaseMutex(); 276 | // 结束程序 277 | return; 278 | } 279 | else 280 | { 281 | Console.WriteLine("莫名其妙的异常,直接退出"); 282 | } 283 | } 284 | private static void DoWork() 285 | { 286 | for (int i = 0; i < 5; i++) 287 | { 288 | Console.WriteLine("子线程工作中"); 289 | Thread.Sleep(TimeSpan.FromSeconds(1)); 290 | } 291 | } 292 | } 293 | } 294 | 295 | ``` 296 | 297 | 然后发布或生成项目,打开程序文件位置,复制线程文件路径。 298 | 创建一个新项目,名为 Parent 的 .NET Core 控制台,其 Program 中的代码如下: 299 | 300 | ```csharp 301 | using System; 302 | using System.Diagnostics; 303 | using System.Threading; 304 | 305 | namespace Parent 306 | { 307 | class Program 308 | { 309 | const string name = "进程同步示例"; 310 | private static Mutex m; 311 | static void Main(string[] args) 312 | { 313 | // 晚一些再执行,我录屏要对正窗口位置 314 | Thread.Sleep(TimeSpan.FromSeconds(3)); 315 | Console.WriteLine("父进程启动!"); 316 | 317 | new Thread(() => 318 | { 319 | // 启动子进程 320 | Process process = new Process(); 321 | process.StartInfo.UseShellExecute = true; 322 | process.StartInfo.CreateNoWindow = false; 323 | process.StartInfo.WorkingDirectory = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1"; 324 | process.StartInfo.FileName = @"../../../ConsoleApp9\Children\bin\Debug\netcoreapp3.1\Children.exe"; 325 | process.Start(); 326 | process.WaitForExit(); 327 | }).Start(); 328 | 329 | 330 | // 子进程启动需要一点时间 331 | Thread.Sleep(TimeSpan.FromSeconds(1)); 332 | 333 | // 获取互斥体 334 | bool firstInstance; 335 | m = new Mutex(true, name, out firstInstance); 336 | 337 | // 说明子进程还在运行 338 | if (!firstInstance) 339 | { 340 | // 等待子进程运行结束 341 | Console.WriteLine("等待子进程运行结束"); 342 | m.WaitOne(); 343 | Console.WriteLine("子进程运行结束,程序将在3秒后自动退出"); 344 | m.ReleaseMutex(); 345 | Thread.Sleep(TimeSpan.FromSeconds(3)); 346 | return; 347 | } 348 | } 349 | } 350 | } 351 | 352 | ``` 353 | 354 | 请将 Children 项目的程序文件路径,替换到 Parent 项目启动子进程的那部分字符串中。 355 | 356 | 然后启动 Parent.exe,可以观察到如下图的运行过程: 357 | 358 | ![进程同步](./images/进程同步.gif) 359 | 360 | 361 | 362 | ### 另外 363 | 364 | 构造函数中,如果为 `name` 指定 `null` 或空字符串,则将创建一个本地 Mutex 对象,只会在进程内有效。 365 | 366 | Mutex 有些使用方法比较隐晦,可以参考 [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.-ctor?view=netcore-3.1#System_Threading_Mutex__ctor_System_Boolean_](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.-ctor?view=netcore-3.1#System_Threading_Mutex__ctor_System_Boolean_) 367 | 368 | 369 | 370 | 另外打开互斥体,请参考 371 | 372 | [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.openexisting?view=netcore-3.1](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.openexisting?view=netcore-3.1) 373 | 374 | [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.tryopenexisting?view=netcore-3.1](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.mutex.tryopenexisting?view=netcore-3.1) 375 | 376 | 377 | 378 | 到目前为止,我们学习了排他锁 lock、Monitor、Mutex。下一篇我们将来学习非排他锁定结构的`Semaphore`和`SemaphoreSlim` 。 379 | 380 | -------------------------------------------------------------------------------- /1.thread_basic/2.thread_model.md: -------------------------------------------------------------------------------- 1 | # 1.2 .NET 多线程模型 2 | 3 | 在第一章中,我们学习了 Thread 类,它是 C# 使用多线程能力的基础,在本章笔者将会从源码角度中解析 C# 的 Thread,而多线程编程属于并发编程,会涉及到很多中断、同步问题如锁、内存屏障等,而本章内容主要帮助读者了解操作系统和 .NET CLR 中的一些知识,便于理解更多细节以及为后面的章节阅读打下基础。 4 | 5 | 本文内容线路可能比较乱,随便写写,随便看看。 6 | 7 | 8 | 9 | ### 并发编程 10 | 11 | **并发跟并行不一样**。并行是真正多核同时运行,多个事件在同一时刻发生;而并发是多个事件在同一个时间间隔内发生,在多线程编程中,我们往往会留意到线程会发生上下文切换。在 .NET 中,有 TPL、Parallel、PLinq 库提供并行编程,而在本章中,讨论的是并发编程。 12 | 13 | 14 | 15 | 《深入理解计算机系统》的 12章中,列举了并发编程的一些场景: 16 | 17 | * **访问慢速 I/O 设备**。当一个应用程序正在等待来着慢速 I/O 设备(如磁盘)的数据到达时,内核会运行其它进程,使 CPU 保持繁忙。每个应用都可以按照类似的方式,通过交替执行 I/O 请求和其他有用的工作来利用并发。 18 | 19 | * **与人交互**。和计算机交互的人要求计算机有同时执行多个任务的能力。例如,他们在打印一个文档时,可能想要调整一个窗口的大小。现代视窗系统利用并发来提供这种能力。每次用户请求某种操作(比如通过单击鼠标)时,一个独立的并发逻辑流被创建来执行这个操作。 20 | 21 | * **通过推迟工作以降低延迟**。有时,应用程序能够通过推迟其他操作和并发地执行它们,利用并发来降低某些操作的延迟。比如,一个动态内存分配器可以通过推迟合并,把它放到一个运行在较低优先级上的并发“合并”流中,在有空闲的CPU周期时充分利用这些空闲周期,从而降低单个 free 操作的延迟。 22 | 23 | * **服务多个网络客户端**。可能期望它每秒为成百上千的客户端提供服务,由于一个慢速客户端导致拒绝为其他客户端服务,这是不能接受一个更好的方法是创建一个并发服务器,它为每个客户端创建一个单独的逻辑这就允许服务器同时为多个客户端服务,并且也避免了慢速客户端独占服务器。 24 | 25 | * **在多核机器上进行并行计算**。许多现代系统都配备多核处理器,多核处理器中包含有多个 CPU。被划分成并发流的应用程序通常在多核机器上比在单处理器机器上运行得快,因为这些流会并行执行,而不是交错执行。 26 | 27 | > 这部分内容来自中文翻译书,可能不太通顺。 28 | 29 | 30 | 31 | 而在现代操作系统中,提供了三种并发编程模型: 32 | 33 | * 进程。进程有操作系统管理,每个进程执行一类任务,多个进程并发运行,提示完成多个任务。 34 | * I/O 多路复用。如 Linux 的 epoll。 35 | * 线程。通用方法。 36 | 37 | 38 | 39 | 本系列文章中,主要关注多线程并发编程模型。接下来,我们将使用 C 语言,通过线程并发模型,来编写一个具有并发能力的程序。通过这个示例代码,在我们了解 CLR 线程代码时,加深理解。 40 | 41 | 42 | 43 | ### 并发编程模型:线程 44 | 45 | 每个进程开始生命周期时都是单一的线程,这个线程称为主线程,在主线程中可以创建新的线程,于是这些线程与主线程一起并发运行。 46 | 47 | 线程就是运行在进程上下文中的逻辑流。每个线程都有它自己的**线程上下文**,**包括线程 ID、栈、栈指针、程序计数器、通用目的寄存器和条件码**,进程中的线程共享该进程的整个虚拟地址空间(虚拟内存)。 48 | 49 | 50 | 51 | 虽然线程比进程廉价,但是发生线程上下文切换时,CPU 会挂起当前线程转而执行其它线程,要先保存原线程的栈、寄存器等,接着加载新的线程的上下文。在这个上下文切换中,是需要消耗时间的,频繁的线程上下文切换,会导致浪费大量性能。而且线程的创建和消耗都需要消耗系统资源,频繁创建线程会导致系统资源消耗过多,当线程数量太多时,线程切换上下文的次数也会变多,消耗大量的 CPU。同时,线程上下文切换会导致 CPU 缓存失效以及命中率降低,执行效率变低。 52 | 53 | 54 | 55 | ### CPU 缓存结构 56 | 57 | 这里聊一下 CPU 的缓存结构,这对帮助我们了解后面的并发编程等有好处。 58 | 59 | 现代 CPU 的高速缓冲存储器一般分为三个级别 L1、L2、L3。 60 | 61 | ![image-20220327152524746](./images/image-20220327152524746.png) 62 | 63 | 【图来自小林《图解操作系统》】 64 | 65 | 66 | 67 | 每个 CPU 核心内部都会有一个 L1 Cache 和 L2 Cache,而所有 CPU 核心共享一个 L3 Cache。 68 | 69 | 其中, L1 Cache 分为**数据缓存和指令缓存**。 70 | 71 | 在 Windows 中,可以通过任务管理器查看每级高速缓冲存储器已用空间。 72 | 73 | ![image-20220327153359457](./images/image-20220327153359457.png) 74 | 75 | 在 Linux 中,我们也可以通过命令查看一个核心中 L1 Cache 的数据缓存和指令缓存空间大小: 76 | 77 | ```bash 78 | whuanle@whuanle-PC:~$ ls /sys/devices/system/cpu/ 79 | cpu0 cpu10 cpu2 cpu4 cpu6 cpu8 cpufreq hotplug kernel_max offline possible smt vulnerabilities 80 | cpu1 cpu11 cpu3 cpu5 cpu7 cpu9 cpuidle isolated modalias online present uevent 81 | whuanle@whuanle-PC:~$ cat /sys/devices/system/cpu/cpu0/cache/index0/size 82 | 32K 83 | whuanle@whuanle-PC:~$ cat /sys/devices/system/cpu/cpu0/cache/index1/size 84 | 32K 85 | ``` 86 | 87 | 查看 L2 Cache 大小: 88 | 89 | ```bash 90 | whuanle@whuanle-PC:~$ cat /sys/devices/system/cpu/cpu0/cache/index2/size 91 | 512K 92 | ``` 93 | 94 | 查看 L3 Cache 大小: 95 | 96 | ```bash 97 | whuanle@whuanle-PC:~$ cat /sys/devices/system/cpu/cpu0/cache/index3/size 98 | 16384K 99 | ``` 100 | 101 | ![image-20220327211531022](./images/image-20220327211531022.png) 102 | 103 | 104 | 105 | > **[info]** 提示 106 | > 107 | > ``` 108 | > L1:index0、index1,数据缓存和指令缓存 109 | > 110 | > L2:index2 111 | > 112 | > L3:index3 113 | > ``` 114 | 115 | 116 | 117 | 如果 CPU 0 核心把一个变量从内存加载到 L3 - L2 - L1 ,然后通过计算,要把值写回内存,而此时 CPU 1 也需要使用到这个变量,发现 L3 已经有了,直接从 L3 中加载,而 CPU 0 计算完成后,把这变量的值覆盖了,接着 CPU 1 又覆盖一次。这样就会导致不一致。C# 中的多线程编程,最简单的是使用 lock 加锁,但是在 CPU 中,要协调多处理器读取同一个变量,则会变得很复杂。 118 | 119 | 120 | 121 | 高速缓冲存储器的分级机制有利于 CPU 提前加载内存中的指令和数据,以及对要执行的指令进行预测,前面说到,频繁进行线程上下文切换,会导致缓存失效, CPU 需要频繁从内存中加载数据和指令到高速缓冲存储器。L1 的速度大约是 2-4 个时钟周期,而 L2 则是 10-20 个时钟周期,L3 是 20-60 个时钟周期,而内存(这里只指DRAM)则可能要上百个时钟周期。 122 | 123 | ![image-20220327211555818](./images/image-20220327211555818.png) 124 | 125 | 注:笔者的内存是 PCIe® 3.0 DDR4,速度快。PCIe 2.0 或 DDR3 内存条会更加慢。 126 | 127 | 128 | 129 | 130 | 131 | ### 创建线程 132 | 133 | 下面是一份 C 语言的代码,使用 Posix 标准的线程库,可以运行在 Unix 类系统 下,通过 Posix,我们可以使用 pthread 来使用创建线程,下面是一个简单的示例: 134 | 135 | ```c 136 | #include "pthread.h" 137 | 138 | void* thread(void* vargp) 139 | { 140 | printf("Hello, world!\n"); 141 | return NULL; 142 | } 143 | 144 | int main() 145 | { 146 | // typedef unsigned long int pthread_t; 147 | pthread_t tid; 148 | Pthread_create(&tid, NULL, thread, NULL); // 创建线程并获得线程的 id 149 | Pthread_join(tid, NULL); // mian 阻塞等待 这个线程完成 150 | exit(0); 151 | } 152 | 153 | /* 154 | extern int pthread_create (pthread_t *__restrict __newthread, 155 | const pthread_attr_t *__restrict __attr, 156 | void *(*__start_routine) (void *), 157 | void *__restrict __arg) __THROWNL __nonnull ((1, 3)); 158 | */ 159 | ``` 160 | 161 | > **[info] 提示** 162 | > 163 | > **POSIX线程**(POSIX Threads 常被缩写为 Pthreads)是 POSIX 的线程标准,定义了创建和操纵线程的一套API。 164 | > 165 | > 实现 POSIX 线程标准的库常被称作 **Pthreads**。 166 | 167 | 168 | 169 | 在很多书籍中,都会讨论到用户空间线程、内核线程、轻量级进程,在新版本的 Linux 内核中,通过 pthread 创建的线程都是内核线程,而这里我们不必想得太复杂,我们可以把使用线程库 POSIX Threads 创建的线程称为原生线程,由操作系统管理。在 Windows 的任务管理器中,便可以看到原生线程的数量。 170 | 171 | ![image-20220327213745505](./images/image-20220327213745505.png) 172 | 173 | 174 | 175 | pthread.h 头文件中,包含了如下功能: 176 | 177 | - 线程管理,例如创建线程,等待(join)线程,查询线程状态等。 178 | - 互斥锁(Mutex):创建、摧毁、锁定、解锁、设置属性等操作 179 | - 条件变量(Condition Variable):创建、摧毁、等待、通知、设置与查询属性等操作 180 | - 使用了互斥锁的线程间的同步管理 181 | 182 | 183 | 184 | 185 | 186 | ### CLR 的线程是如何创建的 187 | 188 | 在 C# 中创建一个线程很简单: 189 | 190 | ```csharp 191 | Thread thread = new Thread(() => { }); 192 | ``` 193 | 194 | 195 | 196 | 在 C# 中创建 Thread 对象是一种轻量级操作,当 `new Thread` 时,只是创建了一个 Thread,此时并不会实际创建一个操作系统线程。**只有使用 `thread.Start();` 时,才会创建一个真正的操作系统线程**。 197 | 198 | 在 `new Thread` 时,CLR 会调用一个 `Initialize` 方法初始化一个 C++ Thread 对象。 199 | 200 | ![image-20220327175223992](./images/image-20220327175223992.png) 201 | 202 | 203 | 204 | 在执行 `SetupUnstartedThread` 时,会创建一个 C++ Thread 对象,并放到 C++ ThreadStore 中,由 ThreadStore 管理 CLR 中的 Thread 对象。这个过程主要为一些字段或属性赋予初始值。`pThis->SetManagedThreadId(unstarted->GetThreadId());` 可以为当前线程生成一个 Id 值。 205 | 206 | 207 | 208 | 一般来说,C# 线程对应的操作系统线程 Id 是不可获取的,但是你可以通过反射获取 CLR 线程对应操作系统线程的 Id: 209 | 210 | ```csharp 211 | static void Main(string[] args) 212 | { 213 | var property = typeof(Thread).GetProperty("CurrentOSThreadId", BindingFlags.NonPublic | BindingFlags.Static); 214 | var id = property.GetValue(null); 215 | Console.WriteLine(id); 216 | } 217 | ``` 218 | 219 | 220 | 221 | 当使用 `thread.Start()` 时,将真正开始创建一个对应的操作系统线程。从 C# 代码到 CLR 的 C++ 代码中,需要调用多个函数才能完成创建流程。 222 | 223 | 具体的创建流程如下所示: 224 | 225 | ![线程创建流程](./images/线程创建流程.png) 226 | 227 | 当执行到 `CreateNewOsThread` 函数时,开始真正创建一个操作系统线程。 228 | 229 | 230 | 231 | ![image-20220327172520496](./images/image-20220327172520496.png) 232 | 233 | 234 | 235 | 首先检查要设置的线程栈空间大小。如果 sizeToCommitOrReserve 是默认值,则使用 `GetDefaultStackSizeSetting()` 方法获取一个线程的栈空间大小。 236 | 237 | 238 | 239 | ```c 240 | SIZE_T GetDefaultStackSizeSetting() 241 | { 242 | static DWORD s_defaultStackSizeEnv = CLRConfig::GetConfigValue(CLRConfig::INTERNAL_DefaultStackSize); 243 | 244 | uint64_t value = s_defaultStackSizeEnv ? s_defaultStackSizeEnv : s_defaultStackSizeProperty; 245 | 246 | SIZE_T minStack = 0x10000; // 64K - Somewhat arbitrary minimum thread stack size 247 | SIZE_T maxStack = 0x80000000; // 2G - Somewhat arbitrary maximum thread stack size 248 | 249 | if ((value >= maxStack) || ((value != 0) && (value < minStack))) 250 | { 251 | ThrowHR(E_INVALIDARG); 252 | } 253 | 254 | return (SIZE_T) value; 255 | } 256 | ``` 257 | 258 | 可知,如果有配置过 appsettings.json 等 程序启动配置文件,那么 GC 会中根据配置文件设置栈大小,当然 CLR 对线程栈空间大小的默认值是 1MB。如果发现程序的 CLR 配置线程栈空间太小或太大(64k-2G),那么就会使用默认值来设置栈大小。 259 | 260 | 261 | 262 | 而在 Unix 类系统下,还要考虑栈空间大小与内存分页页面大小,如果空间大小小于内存页大小,那么以内存页大小为准。 263 | 264 | ```c 265 | 266 | uint32_t GetOsPageSize() 267 | { 268 | #ifdef HOST_UNIX 269 | size_t result = g_pageSize.LoadWithoutBarrier(); 270 | 271 | if(!result) 272 | { 273 | result = GetOsPageSizeUncached(); 274 | 275 | g_pageSize.StoreWithoutBarrier(result); 276 | } 277 | 278 | return result; 279 | #else 280 | return 0x1000; 281 | #endif 282 | } 283 | ``` 284 | 285 | > **[info]** 提示 286 | > 287 | > 默认线程的栈空间的大小为 1MB。我们可以通过在 `new Thread()` 时设置 `maxStackSize` 。 288 | > 289 | > 在 Windows 中内存分配粒度是 64KB,而在 Linux 中需要按照 page 大小对齐,一般情况下都是 4kb。 290 | > 291 | > 在 Linux 中,你可以通过以下命令获取内存页大小。 292 | > 293 | > ``` 294 | > root@whuanle-PC:~# getconf PAGE_SIZE 295 | > 4096 296 | > ``` 297 | 298 | 299 | 300 | 当线程配置准备完成后,CLR 开始真正创建线程,每种操作系统创建的方式不一样。 301 | 302 | CLR 中关于创建线程的代码如下: 303 | 304 | ```c 305 | #ifdef TARGET_UNIX 306 | h = ::PAL_CreateThread64(NULL /*=SECURITY_ATTRIBUTES*/, 307 | #else 308 | h = ::CreateThread( NULL /*=SECURITY_ATTRIBUTES*/, 309 | #endif 310 | sizeToCommitOrReserve, 311 | start, 312 | args, 313 | dwCreationFlags, 314 | &ourId); 315 | ``` 316 | 317 | 下图中,左边是 Windows 的代码,右边是 Linux 代码。 318 | 319 | ![image-20220327173212888](./images/image-20220327173212888.png) 320 | 321 | 322 | 323 | 在 Windows 下,使用 `processthreadsapi.h`,请参考:https://docs.microsoft.com/zh-cn/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread 324 | 325 | 在 Linux 下,使用 pthread,请参考:https://zh.wikipedia.org/wiki/POSIX%E7%BA%BF%E7%A8%8B 326 | 327 | 328 | 329 | 可以看到,Linux 创建线程的方式跟笔者前面使用 C 语言创建线程的方式一样的。这里我们不需要关心 pthread 、kthread 的用户线程和内核线程问题,我们只需要知道 CLR 创建的线程,是可以被操作系统识别的,即原生线程。 330 | 331 | 332 | 333 | ### 线程消耗的堆栈空间 334 | 335 | 通过这些步骤,你应该意识到 C# 中的线程与操作系统线程是一对一的关系。同时你也了解到,创建一个 Thread,需要这么多步骤,而且消耗类系统资源,因此在 .NET 并发编程中,我们要避免直接创建线程,而应该多使用线程池线程或 Task,避免创建和回收线程的开销。 336 | > 当然,CLR 线程不一定需要操作系统线程绑定启动,一个 CLR 线程在生命周期中可能使用不同的操作系统线程执行代码。 337 | > 请参考 CLR 线程模型设计: 338 | > https://github.com/dotnet/coreclr/blob/master/Documentation/botr/threading.md 339 | 340 | 341 | 在 Windows 下, .NET 进程的内存布局如下: 342 | 343 | ![image-20220329063238458](./images/image-20220329063238458.png) 344 | 345 | 【图来源《.NET 内存管理宝典》,图区分 CPU 字长,图左为 32位,图右是 64位】 346 | 347 | 348 | 349 | 在前面,我们已经了解到,创建线程时,会创建 C# 的 Thread 实例、CLR 管理的 C++ Thread 实例,最后创建一个操作系统线程放到 CLR 中管理。其中,C# 创建的对象会在 GC heap 中,而 CLR heap 会管理用于其内部目的各种对象,如 C++ Thread。最后,图中有个 stack,这便是 CLR 进程中存放线程的堆栈区域,可以看到,stack 不止出现在低位地址,还可以出现在任意位置。 350 | 351 | 352 | 353 | 线程的堆栈内存,除了供定义各种变量时使用,还需要存储函数的方法表等信息,此外,递归调用也是非常消耗内存空间的。下面我们通过一个示例来说明 354 | 355 | 356 | 357 | 定义一个 64 字节的结构体。 358 | 359 | ```csharp 360 | public struct Test 361 | { 362 | public long A; 363 | public long B; 364 | public long C; 365 | public long D; 366 | public long E; 367 | public long F; 368 | public long G; 369 | public long H; 370 | } 371 | 372 | /* 373 | static unsafe void Main(string[] args) 374 | { 375 | Console.WriteLine(sizeof(Test)); // 输出 64 376 | Console.ReadKey(); 377 | } 378 | */ 379 | ``` 380 | 381 | 382 | 接下来,我们一个函数中,分配 730 大小的空间: 383 | 384 | ```csharp 385 | static void Main(string[] args) 386 | { 387 | new Thread(() => 388 | { 389 | StackOver(); 390 | }, 64 * 1024) 391 | .Start(); 392 | Console.ReadKey(); 393 | } 394 | 395 | static unsafe void StackOver() 396 | { 397 | Test* test = stackalloc Test[730]; // 729 不会导致堆栈溢出 398 | } 399 | 400 | ``` 401 | 402 | > **[info] 提示** 403 | > 404 | > `stackalloc` 用于在不安全代码中分配空间,如果是值类型,则分配堆栈;如果是引用类型,则分配到堆上。 405 | 406 | 407 | 408 | 结果如图所示: 409 | 410 | ![image-20220329070927419](images/image-20220329070927419.png) 411 | 412 | 413 | 414 | 我们给这个线程设置的最大堆栈大小是 `64*1024` 字节即 64KB,但是我们只分配了 `64*730` ,线程的堆栈便溢出了!也就是说`1024-729=295` 个字节的空间被用于保存线程的寄存器以及方法表方法名称、参数等。 415 | 416 | 所以,记住,给线程设置了堆栈大小,并不代表你定义变量时可以使用这么多的堆栈空间。 417 | 418 | 419 | 420 | 另外,调用函数时,会消耗相当大的一部分堆栈空间。 421 | 422 | 将上面的示例改成: 423 | 424 | ```csharp 425 | static void Main(string[] args) 426 | { 427 | new Thread(() => 428 | { 429 | StackOver(); 430 | }, 64 * 1024) 431 | .Start(); 432 | Console.ReadKey(); 433 | } 434 | 435 | static void StackOver() 436 | { 437 | StackOver(); 438 | } 439 | ``` 440 | 441 | 执行程序后,到 745 次,即会发生堆栈溢出。 442 | 443 | ![image-20220329072022154](images/image-20220329072022154.png) 444 | 445 | 446 | 447 | 这个函数什么都没有做,却把 64KB 的堆栈空间都耗尽了,在 745 次递归调用中,约每次消耗 88 个字节的空间。 448 | 449 | 450 | 451 | 堆栈是一种后进先出(LIFO)的数据结构,一个简化的线程堆栈示例如下: 452 | 453 | ![image-20220329073441047](images/image-20220329073441047.png) 454 | -------------------------------------------------------------------------------- /2.thread_sync/9.reader_writer_lock.md: -------------------------------------------------------------------------------- 1 | # 2.9 读写锁 2 | 3 | ReaderWriterLock、ReaderWriterLockSlim 都是 C# 中设置读写锁的手段,本篇的内容主要是介绍 ReaderWriterLockSlim 类,来实现多线程下的读写分离,由于两者很接近,因此本章就不讨论 ReaderWriterLock 了。 4 | 5 | 6 | 7 | ## ReaderWriterLockSlim 8 | 9 | ReaderWriterLock 类:定义支持单个写线程和多个读线程的锁。 10 | 11 | ReaderWriterLockSlim 类:表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问。 12 | 13 |

14 |

15 | 两者的 API 十分接近,而且 ReaderWriterLockSlim 相对 ReaderWriterLock 来说 更加安全。因此本文主要讲解 ReaderWriterLockSlim 。 16 |
17 |

18 | 19 | 两者都是实现多个线程可同时读取、只允许一个线程写入的类。 20 | 21 | 22 | 23 | ## ReaderWriterLockSlim 24 | 25 | 老规矩,先大概了解一下 ReaderWriterLockSlim 常用的方法。 26 | 27 | ### 常用方法 28 | 29 | | 方法 | 说明 | 30 | | ------------------------------------- | ------------------------------------------------------------ | 31 | | EnterReadLock() | 尝试进入读取模式锁定状态。 | 32 | | EnterUpgradeableReadLock() | 尝试进入可升级模式锁定状态。 | 33 | | EnterWriteLock() | 尝试进入写入模式锁定状态。 | 34 | | ExitReadLock() | 减少读取模式的递归计数,并在生成的计数为 0(零)时退出读取模式。 | 35 | | ExitUpgradeableReadLock() | 减少可升级模式的递归计数,并在生成的计数为 0(零)时退出可升级模式。 | 36 | | ExitWriteLock() | 减少写入模式的递归计数,并在生成的计数为 0(零)时退出写入模式。 | 37 | | TryEnterReadLock(Int32) | 尝试进入读取模式锁定状态,可以选择整数超时时间。 | 38 | | TryEnterReadLock(TimeSpan) | 尝试进入读取模式锁定状态,可以选择超时时间。 | 39 | | TryEnterUpgradeableReadLock(Int32) | 尝试进入可升级模式锁定状态,可以选择超时时间。 | 40 | | TryEnterUpgradeableReadLock(TimeSpan) | 尝试进入可升级模式锁定状态,可以选择超时时间。 | 41 | | TryEnterWriteLock(Int32) | 尝试进入写入模式锁定状态,可以选择超时时间。 | 42 | | TryEnterWriteLock(TimeSpan) | 尝试进入写入模式锁定状态,可以选择超时时间。 | 43 | 44 | 45 | 46 | ReaderWriterLockSlim 的读、写入锁模板如下: 47 | 48 | ```csharp 49 | private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim(); 50 | 51 | // 读 52 | private T Read() 53 | { 54 | try 55 | { 56 | toolLock.EnterReadLock(); // 获取读取锁 57 | return obj; 58 | } 59 | catch { } 60 | finally 61 | { 62 | toolLock.ExitReadLock(); // 释放读取锁 63 | } 64 | return default; 65 | } 66 | 67 | // 写 68 | public void Write(int key, int value) 69 | { 70 | try 71 | { 72 | toolLock.EnterUpgradeableReadLock(); 73 | 74 | try 75 | { 76 | toolLock.EnterWriteLock(); 77 | // 78 | } 79 | catch{} 80 | finally 81 | { 82 | toolLock.ExitWriteLock(); 83 | } 84 | } 85 | catch { } 86 | finally 87 | { 88 | toolLock.ExitUpgradeableReadLock(); 89 | } 90 | } 91 | ``` 92 | 93 | 使用 ReaderWriterLockSlim 时,需要注意释放读锁或写锁。 94 | 95 | 96 | 97 | ### 订单系统示例 98 | 99 | 这里来模拟一个简单粗糙的订单系统。 100 | 101 | 开始编写代码前,先来了解一些方法的具体使用。 102 | 103 | `EnterReadLock()` / `TryEnterReadLock` 和 `ExitReadLock()` 成对出现。 104 | 105 | `EnterWriteLock()` / `TryEnterWriteLock()` 和 `ExitWriteLock()` 成对出现。 106 | 107 | `EnterUpgradeableReadLock()` 进入可升级的读模式锁定状态。 108 | 109 | `EnterReadLock()` 使用 `EnterUpgradeableReadLock()` 进入升级状态,在恰当时间点 通过 `EnterWriteLock()` 进入写模式。(也可以倒过来) 110 | 111 | 112 | 113 | 定义三个变量: 114 | 115 | ReaderWriterLockSlim 多线程读写锁; 116 | 117 | MaxId 当前订单 Id 的最大值; 118 | 119 | orders 订单表; 120 | 121 | 122 | 123 | ```csharp 124 | private static ReaderWriterLockSlim tool = new ReaderWriterLockSlim(); // 读写锁 125 | 126 | private static int MaxId = 1; 127 | public static List orders = new List(); // 订单表 128 | ``` 129 | 130 | ```csharp 131 | // 订单模型 132 | public class DoWorkModel 133 | { 134 | public int Id { get; set; } // 订单号 135 | public string UserName { get; set; } // 客户名称 136 | public DateTime DateTime { get; set; } // 创建时间 137 | } 138 | ``` 139 | 140 | 141 | 142 | 然后实现查询和创建订单的两个方法。 143 | 144 | 分页查询订单: 145 | 146 | 在读取前使用 `EnterReadLock()` 获取锁; 147 | 148 | 读取完毕后,使用 `ExitReadLock()` 释放锁。 149 | 150 | 这样能够在多线程环境下保证每次读取都是最新的值。 151 | 152 | ```csharp 153 | // 分页查询订单 154 | private static DoWorkModel[] DoSelect(int pageNo, int pageSize) 155 | { 156 | 157 | try 158 | { 159 | DoWorkModel[] doWorks; 160 | tool.EnterReadLock(); // 获取读取锁 161 | doWorks = orders.Skip((pageNo - 1) * pageSize).Take(pageSize).ToArray(); 162 | return doWorks; 163 | } 164 | catch { } 165 | finally 166 | { 167 | tool.ExitReadLock(); // 释放读取锁 168 | } 169 | return default; 170 | } 171 | ``` 172 | 173 | 174 | 175 | 创建订单: 176 | 177 | 创建订单的信息十分简单,知道用户名和创建时间就行。 178 | 179 | 订单系统要保证的时每个 Id 都是唯一的(实际情况应该用Guid),这里为了演示读写锁,设置为 数字。 180 | 181 | 在多线程环境下,我们不使用 `Interlocked.Increment()` ,而是直接使用 `+= 1`,因为有读写锁的存在,所以操作也是原则性的。 182 | 183 | ```csharp 184 | // 创建订单 185 | private static DoWorkModel DoCreate(string userName, DateTime time) 186 | { 187 | try 188 | { 189 | tool.EnterUpgradeableReadLock(); // 升级 190 | try 191 | { 192 | tool.EnterWriteLock(); // 获取写入锁 193 | 194 | // 写入订单 195 | MaxId += 1; // Interlocked.Increment(ref MaxId); 196 | 197 | DoWorkModel model = new DoWorkModel 198 | { 199 | Id = MaxId, 200 | UserName = userName, 201 | DateTime = time 202 | }; 203 | orders.Add(model); 204 | return model; 205 | } 206 | catch { } 207 | finally 208 | { 209 | tool.ExitWriteLock(); // 释放写入锁 210 | } 211 | } 212 | catch { } 213 | finally 214 | { 215 | tool.ExitUpgradeableReadLock(); // 降级 216 | } 217 | return default; 218 | } 219 | ``` 220 | 221 | 222 | 223 | Main 方法中: 224 | 225 | 开 5 个线程,不断地读,开 2 个线程不断地创建订单。线程创建订单时是没有设置 `Thread.Sleep()` 的,因此运行速度十分快。 226 | 227 | Main 方法里面的代码没有什么意义。 228 | 229 | ```csharp 230 | static void Main(string[] args) 231 | { 232 | // 5个线程读 233 | for (int i = 0; i < 5; i++) 234 | { 235 | new Thread(() => 236 | { 237 | while (true) 238 | { 239 | var result = DoSelect(1, MaxId); 240 | if (result is null) 241 | { 242 | Console.WriteLine("获取失败"); 243 | continue; 244 | } 245 | foreach (var item in result) 246 | { 247 | Console.Write($"{item.Id}|"); 248 | } 249 | Console.WriteLine("\n"); 250 | Thread.Sleep(1000); 251 | } 252 | }).Start(); 253 | } 254 | 255 | for (int i = 0; i < 2; i++) 256 | { 257 | new Thread(() => 258 | { 259 | while(true) 260 | { 261 | var result = DoCreate((new Random().Next(0, 100)).ToString(), DateTime.Now); // 模拟生成订单 262 | if (result is null) 263 | Console.WriteLine("创建失败"); 264 | else Console.WriteLine("创建成功"); 265 | } 266 | 267 | }).Start(); 268 | } 269 | } 270 | ``` 271 | 272 | 273 | 274 | 在 ASP.NET Core 中,则可以利用读写锁,解决多用户同时发送 HTTP 请求带来的数据库读写问题。 275 | 276 | 这里就不做示例了。 277 | 278 | 279 | 280 | 如果另一个线程发生问题,导致迟迟不能交出写入锁,那么可能会导致其它线程无限等待。 281 | 282 | 那么可以使用 `TryEnterWriteLock()` 并且设置等待时间,避免阻塞时间过长。 283 | 284 | ```csharp 285 | bool isGet = tool.TryEnterWriteLock(500); 286 | ``` 287 | 288 | 289 | 290 | ### 并发字典写示例 291 | 292 | 因为理论的东西,笔者这里不会说太多,主要就是先掌握一些 API(方法、属性) 的使用,然后简单写出示例,后面再慢慢深入了解底层原理。 293 | 294 | 这里来写一个多线程共享使用字典(Dictionary)的使用示例。 295 | 296 | 增加两个静态变量: 297 | 298 | ```csharp 299 | private static ReaderWriterLockSlim toolLock = new ReaderWriterLockSlim(); 300 | private static Dictionary dict = new Dictionary(); 301 | ``` 302 | 303 | 实现一个写操作: 304 | 305 | ```csharp 306 | public static void Write(int key, int value) 307 | { 308 | try 309 | { 310 | // 升级状态 311 | toolLock.EnterUpgradeableReadLock(); 312 | // 读,检查是否存在 313 | if (dict.ContainsKey(key)) 314 | return; 315 | 316 | try 317 | { 318 | // 进入写状态 319 | toolLock.EnterWriteLock(); 320 | dict.Add(key,value); 321 | } 322 | finally 323 | { 324 | toolLock.ExitWriteLock(); 325 | } 326 | } 327 | finally 328 | { 329 | toolLock.ExitUpgradeableReadLock(); 330 | } 331 | } 332 | ``` 333 | 334 | 上面没有 `catch { }` 是为了更好观察代码,因为使用了读写锁,理论上不应该出现问题的。 335 | 336 | 模拟五个线程同时写入字典,由于不是原子操作,所以 sum 的值有些时候会出现重复值。 337 | 338 | 原子操作请参考:[https://www.cnblogs.com/whuanle/p/12724371.html#1,出现问题](https://www.cnblogs.com/whuanle/p/12724371.html#1,出现问题) 339 | 340 | ```csharp 341 | private static int sum = 0; 342 | public static void AddOne() 343 | { 344 | for (int i = 0; i < 100_0000; i++) 345 | { 346 | sum += 1; 347 | Write(sum,sum); 348 | } 349 | } 350 | static void Main(string[] args) 351 | { 352 | for (int i = 0; i < 5; i++) 353 | new Thread(() => { AddOne(); }).Start(); 354 | Console.ReadKey(); 355 | } 356 | ``` 357 | 358 | 359 | 360 | ### ReaderWriterLock 361 | 362 | 大多数情况下都是推荐 ReaderWriterLockSlim 的,而且两者的使用方法十分接近。 363 | 364 | 例如 AcquireReaderLock 是获取读锁,AcquireWriterLock 获取写锁。使用对应的方法即可替换 ReaderWriterLockSlim 中的示例。 365 | 366 | 这里就不对 ReaderWriterLock 进行赘述了。 367 | 368 | 369 | 370 | ReaderWriterLock 的常用方法如下: 371 | 372 | | 方法 | 说明 | 373 | | ----------------------------------- | ------------------------------------------------------------ | 374 | | AcquireReaderLock(Int32) | 使用一个 Int32 超时值获取读线程锁。 | 375 | | AcquireReaderLock(TimeSpan) | 使用一个 TimeSpan 超时值获取读线程锁。 | 376 | | AcquireWriterLock(Int32) | 使用一个 Int32 超时值获取写线程锁。 | 377 | | AcquireWriterLock(TimeSpan) | 使用一个 TimeSpan 超时值获取写线程锁。 | 378 | | AnyWritersSince(Int32) | 指示获取序列号之后是否已将写线程锁授予某个线程。 | 379 | | DowngradeFromWriterLock(LockCookie) | 将线程的锁状态还原为调用 UpgradeToWriterLock(Int32) 前的状态。 | 380 | | ReleaseLock() | 释放锁,不管线程获取锁的次数如何。 | 381 | | ReleaseReaderLock() | 减少锁计数。 | 382 | | ReleaseWriterLock() | 减少写线程锁上的锁计数。 | 383 | | RestoreLock(LockCookie) | 将线程的锁状态还原为调用 ReleaseLock() 前的状态。 | 384 | | UpgradeToWriterLock(Int32) | 使用一个 Int32 超时值将读线程锁升级为写线程锁。 | 385 | | UpgradeToWriterLock(TimeSpan) | 使用一个 `TimeSpan` 超时值将读线程锁升级为写线程锁。 | 386 | 387 | 388 | 389 | 官方示例可以看: 390 | 391 | https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.readerwriterlock?view=netcore-3.1#examples 392 | 393 | 394 | 395 | -------------------------------------------------------------------------------- /2.thread_sync/1.interlocked.md: -------------------------------------------------------------------------------- 1 | # 2.1 原子操作 2 | 3 | ### 导读 4 | 5 | 本章主要讲述多线程竞争下的原子操作。 6 | 7 | 在 1.2 章节的线程模型中,我们了解到了 CPU 缓存机制,在本章中需要应用到这些知识。 8 | 9 | 10 | 11 | ## 知识点 12 | 13 | ### 竞争条件 14 | 15 | 当两个或两个以上的线程访问共享数据,并且尝试同时改变它时,就会发生争用的情况。它们所依赖的那部分共享数据,叫做竞争条件。 16 | 17 | 数据争用是竞争条件中的一种,出现竞争条件可能会导致内存(数据)损坏或者出现不确定性的行为。 18 | 19 | 在操作系统中,会学到临界资源、临界区之类的知识,这里就不提了,只涉及 .NET 部分的概念。 20 | 21 | 22 | 23 | ### 线程同步 24 | 25 | 如果有 N 个线程都会执行某个操作,当一个线程正在执行这个操作时,其它线程都必须依次等待,这就是线程同步。 26 | 27 | 多线程环境下出现竞争条件,通常是没有执行正确的同步而导致的。 28 | 29 | 30 | 31 | ### CPU时间片和上下文切换 32 | 33 | 时间片(timeslice)是操作系统分配给每个正在运行的进程微观上的一段 CPU 时间。 34 | 35 |

36 |

37 | 首先,内核会给每个进程分配相等的初始时间片,然后每个进程轮番地执行相应的时间,当所有进程都处于时间 片耗尽的状态时,内核会重新为每个进程计算并分配时间片,如此往复。 38 |
39 |

40 | 41 | 请参考:[https://zh.wikipedia.org/wiki/%E6%97%B6%E9%97%B4%E7%89%87](https://zh.wikipedia.org/wiki/时间片) 42 | 43 | 44 | 45 | 上下文切换(Context Switch),也称做进程切换或任务切换,是指 CPU 从一个进程或线程切换到另一个进程或线程。 46 | 47 |

48 |

49 | 在接受到中断(Interrupt)的时候,CPU 必须要进行上下文交换。进行上下文切换时,会带来性能损失。 50 |
51 |

52 | 53 | 54 | 请参考[[https://zh.wikipedia.org/wiki/上下文交換](https://zh.wikipedia.org/wiki/上下文交換) 55 | 56 | ### 阻塞 57 | 58 | 阻塞状态指线程处于等待状态。当线程处于阻塞状态时,会尽可能少占用 CPU 时间。 59 | 60 | 当线程从运行状态(Runing)变为阻塞状态时(WaitSleepJoin),操作系统就会将此线程占用的 CPU 时间片分配给别的线程。当线程恢复运行状态时(Runing),操作系统会重新分配 CPU 时间片。 61 | 62 | 分配 CPU 时间片时,会出现上下文切换。 63 | 64 | 65 | 66 | ### 内核模式和用户模式 67 | 68 | 只有操作系统才能切换线程、挂起线程,因此阻塞线程是由操作系统处理的,这种方式被称为内核模式(kernel-mode)。 69 | 70 | `Sleep()`、`Join()` 等,都是使用内核模式来阻塞线程,实现线程同步(等待)。 71 | 72 |

73 |

74 | 内核模式实现线程等待时,出现上下文切换。这适合等待时间比较长的操作,这样会减少大量的 CPU 时间损耗。 75 |
76 |

77 | 78 | 如果线程只需要等待非常微小的时间,阻塞线程带来的上下文切换代价会比较大,这时我们可以使用**自旋**,来实现线程同步,这一方法称为用户模式(user-mode)。 79 | 80 | 81 | 82 | 83 | 84 | ## Interlocked 类 85 | 86 | Interlocked 为多个线程共享的变量提供原子操作。 87 | 88 | 使用 Interlocked 类 避免竞争条件,可以在不阻塞线程(lock、Monitor)的情况下,对目标对象做修改。 89 | 90 | 91 | 92 | Interlocked 类是静态类,让我们先来看看 Interlocked 的常用方法: 93 | 94 | | 方法 | 作用 | 95 | | ----------------- | ------------------------------------------------------------ | 96 | | CompareExchange() | 比较两个数是否相等,如果相等,则替换第一个值。 | 97 | | Decrement() | 以原子操作的形式递减指定变量的值并存储结果。 | 98 | | Exchange() | 以原子操作的形式,设置为指定的值并返回原始值。 | 99 | | Increment() | 以原子操作的形式递增指定变量的值并存储结果。 | 100 | | Add() | 对两个数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。 | 101 | | Read() | 返回一个以原子操作形式加载的值。 | 102 | 103 | 全部方法请查看:[https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1#methods](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.interlocked?view=netcore-3.1#methods) 104 | 105 | 106 | 107 | ### 1,CPU 缓存问题 108 | 109 | 问题: 110 | 111 | ​ C# 中赋值和一些简单的数学运算不是原子操作,受多线程环境影响,可能会出现问题。 112 | 113 | 我们可以使用 lock 和 Monitor 来解决这些问题,但是还有没有更加简单的方法呢? 114 | 115 | 首先我们编写以下代码: 116 | 117 | ```csharp 118 | private static int sum = 0; 119 | public static void AddOne() 120 | { 121 | for (int i = 0; i < 100_0000; i++) 122 | { 123 | sum += 1; 124 | } 125 | } 126 | ``` 127 | 128 | 这个方法的工作完成后,sum 会 +100。 129 | 130 | 我们在 Main 方法中调用: 131 | 132 | ```csharp 133 | static void Main(string[] args) 134 | { 135 | AddOne(); 136 | AddOne(); 137 | AddOne(); 138 | AddOne(); 139 | AddOne(); 140 | Console.WriteLine("sum = " + sum); 141 | } 142 | ``` 143 | 144 | 结果肯定是 5000000,无可争议的。 145 | 146 | 但是这样会慢一些,如果作死,要多线程同时执行呢? 147 | 148 | 好的,Main 方法改成如下: 149 | 150 | ```csharp 151 | static void Main(string[] args) 152 | { 153 | for (int i = 0; i < 5; i++) 154 | { 155 | Thread thread = new Thread(AddOne); 156 | thread.Start(); 157 | } 158 | 159 | Thread.Sleep(TimeSpan.FromSeconds(2)); 160 | Console.WriteLine("sum = " + sum); 161 | } 162 | ``` 163 | 164 | 165 | 166 | 笔者运行一次,出现了 `sum = 2633938` 167 | 168 | 我们将每次运算的结果保存到数组中,截取其中一段发现: 169 | 170 | ``` 171 | 8757 172 | 8758 173 | 8760 174 | 8760 175 | 8760 176 | 8761 177 | 8762 178 | 8763 179 | 8764 180 | 8765 181 | 8766 182 | 8766 183 | 8768 184 | 8769 185 | ``` 186 | 187 | 多个线程使用同一个变量进行操作时,并不知道此变量已经在其它线程中发生改变,导致执行完毕后结果不符合期望。 188 | 189 | 我们可以通过下面这张图来解释: 190 | 191 | ![竞争条件](./images/image-1587174060064.png) 192 | 193 | 操作值类型时,其内存位置的值会被复制到 CPU 缓存中,例如 CPU1 将 sum 值复制到 L2 中,CPU1 还没有将计算后的结果放到 L3, CPU2 读取了一个旧值,即 CPU2 发生脏读,此时 CPU1、CPU2 操作的 `int sum` 导致数据混乱。 194 | 195 | ![image-20220327152524746](images/image-20220327152524746.png) 196 | 197 | 198 | 199 | 200 | 201 | 因此,这里就需要原子操作,在某个时刻,必须只有一个线程能够进行某个操作。而上面的操作,指的是读取、计算、写入这一过程。 202 | 203 | 当然,我们可以使用 lock 或者 Monitor 来解决,但是这样会带来比较大的性能损失。 204 | 205 | 这时 Interlocked 就起作用了,对于一些简单的操作运算, Interlocked 可以实现原子性的操作。 206 | 207 |

208 |

209 | 实现原子性,可以通过多种锁来解决,目前我们学习到了 lock、Monitor,现在来学习 Interlocked ,后面会学到更加多的锁的实现。 210 |
211 |

212 | 213 | 214 | 215 | ### 2,Interlocked.Increment() 216 | 217 | 用于自增操作。 218 | 219 | 我们修改一下 AddOne 方法: 220 | 221 | ```csharp 222 | public static void AddOne() 223 | { 224 | for (int i = 0; i < 100_0000; i++) 225 | { 226 | Interlocked.Increment(ref sum); 227 | } 228 | } 229 | ``` 230 | 231 | 然后运行,你会发现结果 sum = 5000000 ,这就对了。 232 | 233 | 说明 Interlocked 可以对简单值类型进行原子操作。 234 | 235 |

236 |

237 | Interlocked.Increment() 是递增,而 Interlocked.Decrement() 是递减。 238 |
239 |

240 | 241 | 242 | 243 | ### 3,Interlocked.Exchange() 244 | 245 | `Interlocked.Exchange()` 实现赋值运算。 246 | 247 | 这个方法有多个重载,我们找其中一个来看看: 248 | 249 | ```csharp 250 | public static int Exchange(ref int location1, int value); 251 | ``` 252 | 253 | 意思是将 value 赋给 location1 ,然后返回 location1 改变之前的值。 254 | 255 | 测试: 256 | 257 | ```csharp 258 | static void Main(string[] args) 259 | { 260 | int a = 1; 261 | int b = 5; 262 | 263 | // a 改变前为1 264 | int result1 = Interlocked.Exchange(ref a, 2); 265 | 266 | Console.WriteLine($"a新的值 a = {a} | a改变前的值 result1 = {result1}"); 267 | 268 | Console.WriteLine(); 269 | 270 | // a 改变前为 2,b 为 5 271 | int result2 = Interlocked.Exchange(ref a, b); 272 | 273 | Console.WriteLine($"a新的值 a = {a} | b不会变化的 b = {b} | a 之前的值 result2 = {result2}"); 274 | } 275 | ``` 276 | 277 | 278 | 279 | 另外 `Exchange()` 也有对引用类型的重载: 280 | 281 | ```csharp 282 | Exchange(T, T) 283 | ``` 284 | 285 | 286 | 287 | ### 4,Interlocked.CompareExchange() 288 | 289 | 其中一个重载: 290 | 291 | ```csharp 292 | public static int CompareExchange (ref int location1, int value, int comparand) 293 | ``` 294 | 295 | 比较两个 32 位有符号整数是否相等,如果相等,则替换第一个值。 296 | 297 | 如果 `comparand` 和 `location1` 中的值相等,则将 `value` 存储在 `location1`中。 否则,不会执行任何操作。 298 | 299 | 看准了,是 `location1` 和 `comparand` 比较! 300 | 301 | 302 | 303 | 使用示例如下: 304 | 305 | ```csharp 306 | static void Main(string[] args) 307 | { 308 | int location1 = 1; 309 | int value = 2; 310 | int comparand = 3; 311 | 312 | Console.WriteLine("运行前:"); 313 | Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand}"); 314 | 315 | Console.WriteLine("当 location1 != comparand 时"); 316 | int result = Interlocked.CompareExchange(ref location1, value, comparand); 317 | Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand} | location1 改变前的值 {result}"); 318 | 319 | Console.WriteLine("当 location1 == comparand 时"); 320 | comparand = 1; 321 | result = Interlocked.CompareExchange(ref location1, value, comparand); 322 | Console.WriteLine($" location1 = {location1} | value = {value} | comparand = {comparand} | location1 改变前的值 {result}"); 323 | } 324 | ``` 325 | 326 | 327 | 328 | ### 5,Interlocked.Add() 329 | 330 | 对两个 32 位整数进行求和并用和替换第一个整数,上述操作作为一个原子操作完成。 331 | 332 | ```csharp 333 | public static int Add (ref int location1, int value); 334 | ``` 335 | 336 | 只能对 int 或 long 有效。 337 | 338 | 339 | 340 | 回到第一小节的多线程求和问题,使用 `Interlocked.Add()` 来替换`Interlocked.Increment()`。 341 | 342 | ```csharp 343 | static void Main(string[] args) 344 | { 345 | for (int i = 0; i < 5; i++) 346 | { 347 | Thread thread = new Thread(AddOne); 348 | thread.Start(); 349 | } 350 | 351 | Thread.Sleep(TimeSpan.FromSeconds(10)); 352 | Console.WriteLine("sum = " + sum); 353 | } 354 | private static int sum = 0; 355 | public static void AddOne() 356 | { 357 | for (int i = 0; i < 100_0000; i++) 358 | { 359 | Interlocked.Add(ref sum,1); 360 | } 361 | } 362 | ``` 363 | 364 | 365 | 366 | ### 6,Interlocked.Read() 367 | 368 | 返回一个以原子操作形式加载的 64 位值。 369 | 370 | 64位系统上不需要 Read 方法,因为64位读取操作已是原子操作。 在32位系统上,64位读取操作不是原子操作,除非使用 Read 执行。 371 | 372 | ```csharp 373 | public static long Read (ref long location); 374 | ``` 375 | 376 | 就是说 32 位系统上才用得上。 377 | 378 | 具体场景我没有找到。 379 | 380 | 你可以参考一下 https://www.codenong.com/6139699/ 381 | 382 | 貌似没有多大用处?那我懒得看了。 383 | 384 | ![file](./images/image-1586660083905.png) 385 | 386 | ### volatile 387 | 388 | 这里直接引用官方文档的解释: 389 | 390 | `volatile` 关键字指示一个字段可以由多个同时执行的线程修改。 出于性能原因,编译器,运行时系统甚至硬件都可能重新排列对存储器位置的读取和写入。 391 | 392 | 393 | 394 | 据官方文档,`volatile` 关键字可应用于以下类型的字段: 395 | 396 | - 引用类型。 397 | - 指针类型(在不安全的上下文中)。 请注意,虽然指针本身可以是可变的,但是它指向的对象不能是可变的。 换句话说,不能声明“指向可变对象的指针”。 398 | - 简单类型,如 `sbyte`、`byte`、`short`、`ushort`、`int`、`uint`、`char`、`float` 和 `bool`。 399 | - 具有以下基本类型之一的 `enum` 类型:`byte`、`sbyte`、`short`、`ushort`、`int` 或 `uint`。 400 | - 已知为引用类型的泛型类型参数。 401 | - IntPtr 和 UIntPtr。 402 | 403 | 404 | 405 | 注意,volatile 只能用于引用类型或等于小于 32 位的值类型。 406 | 407 | ![image-20220731102225174](images/image-20220731102225174.png) 408 | 409 | 410 | 411 | .NET 设计上是支持 32 位系统和 64 位系统的,而 long 类型固定是 64 位,在 32 位的系统下,CPU 寄存器执行指令时,寄存器一次只能处理 4 字节,要处理 long 类型,取内存就需要两次指令,高 4 字节和 低 4 字节是分开计算的,因此无法保证 long 的原子性。另外,在 32 位系统下,需要额外使用其它指令配合计算 long ,因此也会消耗一部分性能。 412 | 413 | 最简单的例子就是学 C 语言时,都会学到的结构体对齐。 414 | 415 | 416 | 417 | 奇怪,既然 long 、double 不行,为啥引用类型可以? 418 | 419 | 因为 `private volatile object obj;` 保存的是对象的引用地址,其地址长度跟 CPU 有关,取地址值时,只需要一次取数据指令即可。 420 | 421 | 422 | 423 | 关于 volatile 的使用方法有几个误区,第一个误区是, volatile 不能保证数据隔离。 424 | 425 | 如果将上面的示例改成用 volatile ,是否可以正常?答案是**不能**。 426 | 427 | ```csharp 428 | private static volatile int sum = 0; 429 | public static void AddOne() 430 | { 431 | for (int i = 0; i < 100_0000; i++) 432 | { 433 | sum += 1; 434 | } 435 | } 436 | ``` 437 | 438 | ![image-20220731105229745](images/image-20220731105229745.png) 439 | 440 | 441 | 442 | volatile 可以保证同一个字段被多个线程修改时,修改后的最新值能够被线程看到,避免发生脏读,**它无法解决多个线程同时写的问题**。 443 | 444 | 下面举一个不恰当的示例: 445 | 446 | ```csharp 447 | public interface ITest 448 | { 449 | int Run(); 450 | } 451 | public class A : ITest 452 | { 453 | public int Run() => 0; 454 | } 455 | public class B : ITest 456 | { 457 | public int Run() => 1; 458 | } 459 | 460 | private static volatile ITest _test; 461 | static void Main(string[] args) 462 | { 463 | new Thread(() => 464 | { 465 | bool isA = true; 466 | while (true) 467 | { 468 | if (isA) _test = new A(); 469 | else _test = new B(); 470 | isA = !isA; 471 | } 472 | }).Start(); 473 | } 474 | ``` 475 | 476 | 当 _test 指向的对象引用被修改时,其它线程能够及时知道最新的引用对象。 477 | 478 | 479 | 480 | 当一个值被修改时,其它线程及时进行对应操作。 481 | 482 | ```csharp 483 | private static volatile bool IsStop; 484 | static void Main(string[] args) 485 | { 486 | while (!IsStop) 487 | { 488 | } 489 | } 490 | ``` 491 | 492 | 493 | 494 | 初始赋值: 495 | 496 | ```csharp 497 | public class A 498 | { 499 | private static volatile object _obj; 500 | static A() 501 | { 502 | if (_obj == null) 503 | { 504 | _obj = new object(); 505 | } 506 | } 507 | } 508 | ``` 509 | 510 | -------------------------------------------------------------------------------- /3.task/5.async_await.md: -------------------------------------------------------------------------------- 1 | # 3.5 async 和 await 2 | 3 | 4 | ### 导读 5 | 6 | 扯淡了 这么多 篇,这篇终于开始学习 async 和 await 了。在前面我们已经学会了 Task 的各种 API,有了前面的基础,来理解 async 和 await 就容易理解多了。 7 | 8 | 9 | 10 | 本篇讲解 async、await 的简单使用。这一篇一定要按照每一个示例,去写代码、执行、输出结果,自己尝试分析思路。 11 | 12 | 13 | 14 | 使用 `async/await` 的时候,初学者常常会出现很多使用误区。这里,你会跟笔者从以往文章中学习到的知识,去推导,去理解 async 和 await 这两个关键字是如何使用的,又应该怎么合理使用。,我们要一步步来从 Task 中的同步异步开始,慢慢摸索,去分析 async 和 await 两个关键字给我们的异步编程带来了什么样的便利。 15 | 16 | 17 | 18 | ### async 19 | 20 | 微软文档:使用 `async` 修饰符可将方法、lambda 表达式或匿名方法指定为异步。 21 | 22 | 使用 async 修饰的方法,称为异步方法。 23 | 24 | 例如: 25 | 26 | 为了命名规范,使用 async 修饰的方法,需要在方法名称后面加上 `Async` 。 27 | 28 | ```csharp 29 | public async Task TestAsync() 30 | { 31 | // . . . . 32 | } 33 | ``` 34 | 35 | 36 | 37 | 平时编写和使用一个方法时,我们只需要直接使用方法名称就行了。但是如果我们要在一个委托或 Lambda 中使用异步方法,可以在参数之前加上 async 关键字: 38 | 39 | ```csharp 40 | static void Main() 41 | { 42 | // 示例 1, 43 | Thread thread = new Thread(async () => 44 | { 45 | await Task.Delay(0); 46 | }); 47 | } 48 | 49 | // 示例 2 50 | public static async Task TestAsync() => 666; 51 | 52 | public async Task M2() 53 | { 54 | // 示例 3 55 | test = async (a, b) => 56 | { 57 | Console.WriteLine(a + b); 58 | }; 59 | } 60 | 61 | Action test; 62 | ``` 63 | 64 | 65 | 66 | 在上面三个示例中,无论是哪种方式,它们的本质都是定义一个函数,只是编写形式有点不同,所以在定义函数时,我们加上 async 关键字,表示这个方法是异步函数或异步匿名函数。 67 | 68 | 69 | 70 | 一个在类里面的正经的异步函数: 71 | 72 | ```csharp 73 | public static async Task TestAsync() 74 | { 75 | return 666; 76 | } 77 | ``` 78 | 79 | 定义一个委托为异步委托: 80 | 81 | ```csharp 82 | Action test = async (a, b) => 83 | { 84 | Console.WriteLine(a + b); 85 | }; 86 | ``` 87 | 88 | 定义匿名函数是异步函数: 89 | 90 | ```csharp 91 | Thread thread = new Thread(async () => 92 | { 93 | await Task.Delay(0); 94 | }); 95 | ``` 96 | 97 | 98 | 99 | ### await 100 | 101 | 微软文档:`await` 运算符暂停对其所属的 `async` 方法的求值,直到其操作数表示的异步操作完成。 102 | 103 | 104 | 105 | 下面是一个示例: 106 | 107 | ```csharp 108 | public async Task M2() 109 | { 110 | var value = await File.ReadAllTextAsync("a.txt"); 111 | Console.WriteLine(value); 112 | } 113 | ``` 114 | 115 | 116 | 117 | 首先前提是,await 是语法糖,我们编写代码后,编译器首先会将 await 关键字转换为对应的 C# 语法代码,然后编译器在将去掉语法糖之后的代码编译。所以当我们使用 await 时,**要从去掉语法糖之后的代码上理解**,而替换 await 语法糖之后的代码实现,叫**状态机**。在后面的章节中,我们会上手写一个状态机去替代 await 语法糖,以便加深我们的理解,但是在本章中,我们大概了解即可。 118 | 119 | 图:状态机部分代码。 120 | 121 | ![image-20230723152844855](images/image-20230723152844855.png) 122 | 123 | 124 | 125 | 如果用伪代码表示的话,这个代码分为两步。 126 | 127 | 1,调用读取文件的接口,并设置回调函数。 128 | 129 | 2,等待回调函数完成后,继续执行代码。 130 | 131 | ```csharp 132 | public async Task M2() 133 | { 134 | ReadFileEx("D:/a.txt", M_2); // M_2 代码会被系统执行 135 | 等待回调事件完成(); 136 | // 继续执行其它代码 137 | } 138 | public void M_2(string value) 139 | { 140 | Console.WriteLine(value); 141 | } 142 | ``` 143 | 144 | 145 | 146 | 如果我们不使用 await 去写异步代码,我们会面临一个很重要的问题,我们如果知道回调事件已经完成,然后继续执行下面的代码? 147 | 148 | 难道手写循环?如下: 149 | 150 | ```csharp 151 | 152 | bool ok = false; 153 | public async Task M2() 154 | { 155 | ReadFileEx("D:/a.txt", M_2); // M_2 代码会被系统执行 156 | 157 | while(!ok){} 158 | // 继续执行其它代码 159 | } 160 | public void M_2(string value) 161 | { 162 | Console.WriteLine(value); 163 | ok = true; 164 | } 165 | ``` 166 | 167 | 但是 M_2 是在后台执行的,如果出现了异常,那么 ok 这个变量永远不可能是 true,M2 就会陷入无限等待。 168 | 169 | 170 | 171 | 所以,async 、await 可以大大简化我们的代码,我们根本不需要写那么多代码,语法糖帮我们完成回调封装、等待。 172 | 173 | 174 | 175 | 好的,到此为止,async 和 await ,就先说这么多。 176 | 177 | 178 | 179 | 180 | 181 | ### 线程同步 182 | 183 | 在前面的第二大部分中,笔者花了大量的章节解决线程同步问题。 184 | 185 | 到了第三部分,我们开始讲 async、await 两个关键字,依然包括线程同步。 186 | 187 | 编写 async、await 代码时,我们有两大事项要去完成: 188 | 189 | 1,异步,即在 IO 完成后,自动执行代码。实际上就是回调。 190 | 191 | 2,等待回调完成,涉及多个线程的协调,这里就是线程同步。 192 | 193 | 194 | 195 | 本小节演示如何通过 async、await 实现线程同步。 196 | 197 | 198 | 199 | 首先,如果使用 async、await,而是使用 Task 的一些函数,我们来看看如果完成实现下面的场景。 200 | 201 | 202 | 203 | 场景:下单点外卖,在等外卖的时候玩一把王者(只打一把)。如果打完一把游戏,外卖还没有到,就一直等。如果外卖到了,就直接下楼拿外卖。 204 | 205 | 我们可以编写一个示例如下: 206 | 207 | ```csharp 208 | static void Main() 209 | { 210 | Console.WriteLine("点击下单外卖"); 211 | 212 | // 洗衣机在后台工作 213 | Task task = new Task(() => 214 | { 215 | // 模拟外卖员送快递的时间 216 | int time = new Random().Next(2, 6); 217 | Thread.Sleep(TimeSpan.FromSeconds(time)); 218 | return time; 219 | }); 220 | 221 | Console.WriteLine("快递员开始配送"); 222 | task.Start(); 223 | 224 | Console.WriteLine("无聊中,开始打王者"); 225 | 226 | // 打王者 227 | Thread.Sleep(TimeSpan.FromSeconds(4)); 228 | 229 | Console.WriteLine("打完王者了,看看外卖到了没有?"); 230 | 231 | 232 | while (task.IsCompleted == true) 233 | { 234 | Console.WriteLine("外卖到了"); 235 | } 236 | } 237 | ``` 238 | 239 | 240 | 241 | 在这个例子中,我们使用 Task 模拟配送员的线程,当前主线程完成自己的工作后,就会检查配送员线程是否已经完成。 242 | 243 | `while (task.IsCompleted == true)` 这里的代码是为了进行线程同步,对齐多个线程之间的工作。 244 | 245 | 246 | 247 | 如果我们不想自己写一个轮询,我们可以使用官方的 `Wait()`。这是第二个版本。 248 | 249 | ```csharp 250 | task.Wait(); 251 | Console.WriteLine("外卖到了"); 252 | //while (task.IsCompleted == true) 253 | //{ 254 | // Console.WriteLine("外卖到了"); 255 | //} 256 | ``` 257 | 258 | 259 | 260 | 接下来是第三个优化版本,使用 async、await。 261 | 262 | ```csharp 263 | static async Task Main() 264 | { 265 | Console.WriteLine("点击下单外卖"); 266 | 267 | // 洗衣机在后台工作 268 | Task task = new Task(() => 269 | { 270 | // 模拟外卖员送快递的时间 271 | int time = new Random().Next(2, 6); 272 | Thread.Sleep(TimeSpan.FromSeconds(time)); 273 | return time; 274 | }); 275 | 276 | Console.WriteLine("快递员开始配送"); 277 | task.Start(); 278 | 279 | Console.WriteLine("无聊中,开始打王者"); 280 | 281 | // 打王者 282 | Thread.Sleep(TimeSpan.FromSeconds(4)); 283 | 284 | Console.WriteLine("打完王者了,看看外卖到了没有?"); 285 | 286 | await task; 287 | } 288 | ``` 289 | 290 | 291 | 292 | 你看,这样是不是简单多了。 293 | 294 | 295 | 296 | ### 如果不需要线程同步 297 | 298 | 如果你不需要线程同步,那你完全可以丢弃 Task。 299 | 300 | 比如编写一个 http 客户端,对 google 发起一个请求,不管请求成功还是失败。 301 | 302 | ```csharp 303 | _ = new HttpClient().GetAsync("https://www.google.com"); 304 | ``` 305 | 306 | 307 | 308 | 或者对异步方法使用 `async void` 而不是 `async Task`,这样调用者完全无法等待此方法。 309 | 310 | ```csharp 311 | static async Task Main() 312 | { 313 | TestAsync(); 314 | } 315 | 316 | public static async void TestAsync() 317 | { 318 | await new HttpClient().GetAsync("https://www.google.com"); 319 | } 320 | ``` 321 | 322 | 323 | 324 | 但是不推荐使用 `async void`。 325 | 326 | > 后面会解释为什么不推荐使用。 327 | 328 | 329 | 330 | 封装异步方法的规范做法是,返回 `Task`,调用者丢弃 `Task` 即可,即**弃元**,不使用 `await` 关键字等待就行。 331 | 332 | ``` 333 | _ = 异步方法(); 334 | ``` 335 | 336 | 337 | 338 | ### 封装后台任务 339 | 340 | 使用效果跟 `new Thread();` 差不多。 341 | 342 | 前面,我们都是使用了 `new Task()` 来创建任务,而且微软官网大多使用 `Task.Run()` 来编写 async 和 await 的示例。 343 | 344 | 因此,我们可以修改前面的异步任务,改成: 345 | 346 | ```csharp 347 | /// 348 | /// 可异步可同步 349 | /// 350 | /// 351 | public static async Task TestAsync() 352 | { 353 | return await Task.Run(() => 354 | { 355 | return 666; 356 | }); 357 | } 358 | ``` 359 | 360 | 361 | 362 | ### `async Task` 污染 363 | 364 | 我们已经学习了这么多的任务(Task)知识,这一点十分容易解释。 365 | 366 | 因为使用了 async 和 await 关键字,代码最深处,必定会出现 Task 这个东西,Task 这个东西本来就是异步。碰到 await 出现异步,不是因为 await 的作用,而是因为最底层有个 Task。 367 | 368 | ![file](./images/image-1588497534212.png) 369 | 370 | ![file](./images/image-1588498203820.png) 371 | 372 | 373 | 374 | 如果你的一个方法使用了 `async Task`,那么调用了这个方法的其它方法,可能都需要使用 `async Task`,一定程度上会导致 ”代码污染“。 375 | 376 | 377 | 378 | ### async void 379 | 380 | 使用了 `async viod` 修饰的方法,它并不返回 Task,所以我们无法控制这个任务。 381 | 382 | 383 | 384 | 其实就一个区别。 385 | 386 | `async void` 不返回 Task,因为它是使用 “另一个线程” 运行的,所以我们看不到它的运行状态,也就不能线程同步。 387 | 388 | `async Task` 会 Task,我们可以使用 await 等待 Task 完成,或者通过 Task 判断执行是否成功、是否有异常,以及还可以中断 Task 的运行。 389 | 390 | 391 | 392 | 393 | 394 | `async void` 跟 `async Task` 的示例: 395 | 396 | ```csharp 397 | public async void M1() 398 | { 399 | var value = await File.ReadAllTextAsync("a.txt"); 400 | } 401 | public async Task M2() 402 | { 403 | var value = await File.ReadAllTextAsync("a.txt"); 404 | } 405 | ``` 406 | 407 | ![1690100215198](images/1690100215198.png) 408 | 409 | 410 | 411 | 笔者不推荐 `async void` ,还包括无脑 `_ = Task`。 412 | 413 | 在编写 ASP.NET Core 程序时,最常出现两种情况。 414 | 415 | 1,不使用 await 等待方法结束,而这个方法使用了 Scope 作用域的对象。 416 | 417 | ```csharp 418 | public class UserRepository : IUserRepository 419 | { 420 | private readonly IHttpContextAccessor _httpContextAccessor; 421 | privare readonly ILogger _logger; 422 | private readonly DBContext _context; 423 | ... ... 424 | 425 | public async Task LoginAsync(string name, string password) 426 | { 427 | /* 登录验证 */ 428 | 429 | _ = PrintLogAsync(name); 430 | _ = PrintLogAsync(name,true); 431 | return true; 432 | } 433 | 434 | // 打印登录日志 435 | private async Task PrintLogAsync(string name) 436 | { 437 | var value = _httpContextAccessor.HttpContext.xxxxx; 438 | _logger.Debug(value); 439 | } 440 | 441 | // 将登录日志存储到数据库 442 | private async Task WriteLoginRecordAsync(string name,bool isSUccess) 443 | { 444 | _context.LoginRecord.Add(new {name,isSuccess}); 445 | } 446 | } 447 | ``` 448 | 449 | 450 | 451 | 很多人会这样写,理由是不等待其它方法执行完成,这样 LoginAsync 执行就会非常快。 452 | 453 | 但是从这里的代码可以看出两个问题。 454 | 455 | 第一个,HttpContext、Request 这些对象都是 scope 生命周期的,也就是说,如果当前请求已经完成,那么这些对象都会被释放。 456 | 457 | 所以,如果 LoginAsync 已经结束,并且当前请求已经完成,那么再使用任何 request 、response、context 的对象,都会导致报错,因为这些对象都已经释放了。 458 | 459 | 第二个,自己注入的方法,生命周期也是 scope 的,同样会在当前请求结束之后,容器中的 scope 生命周期的对象都会被回收,如果线程挂在后台执行,实现了这些对象,那么依然会出现错误。 460 | 461 | 462 | 463 | 2,事务。 464 | 465 | ```csharp 466 | public class UserRepository : IUserRepository 467 | { 468 | private readonly IHttpContextAccessor _httpContextAccessor; 469 | privare readonly ILogger _logger; 470 | private readonly DBContext _context; 471 | ... ... 472 | 473 | public async Task LoginAsync(string name, string password) 474 | { 475 | // 开启事务 476 | using var tran = _context.BeginTran(); 477 | 478 | _ = PrintLogAsync(name,true); 479 | 480 | tran.End(); 481 | return true; 482 | } 483 | 484 | // 将登录日志存储到数据库 485 | private async Task WriteLoginRecordAsync(string name,bool isSUccess) 486 | { 487 | _context.LoginRecord.Add(new {name,isSuccess}); 488 | } 489 | } 490 | ``` 491 | 492 | 493 | 494 | 在事务中,后台执行异步代码而不等待,是非常愚蠢的做法。因为当 LoginAsync 中的事务已经结束了,WriteLoginRecordAsync 可能才开始执行,但是已经事务已经结束了,再进行数据库操作,会导致出错。 495 | 496 | 497 | 498 | 不过,还有很多情况使用 `_ = Task` 是很有用的。比如需要后台执行任务,而且不太涉及共享变量以及作用域的、纯计算型的任务等等。 499 | 500 | 至于什么时候该用 await ,什么时候可以忽略,这就需要程序员的经验判断了。 501 | 502 | 503 | 504 | 因为不使用 await 时, A 方法调用 B 异步方法, A、B 方法会并发执行,这样的话会大大缩短执行时间。 505 | 506 | 比如,并发请求所有网站: 507 | 508 | ```csharp 509 | async Task Main() 510 | { 511 | var list = new string[] 512 | { 513 | "https://baidu.com", 514 | "https://google.com" 515 | // ......... 516 | }; 517 | 518 | var taskList = new List(); 519 | foreach (var item in list) 520 | { 521 | var task = new System.Net.Http.HttpClient().GetAsync(item); 522 | taskList.Add(task); 523 | } 524 | 525 | await Task.WhenAll(taskList); 526 | } 527 | ``` 528 | 529 | 530 | 531 | 532 | 533 | ### 异步 IO 534 | 535 | 前面以及大概学习了 async 、await 的用法。 536 | 537 | 有很多人以为,使用了 async、await 就能提高性能,提升速度。其实这是两个很大的误区。 538 | 539 | 540 | 541 | 误区一:提高性能。 542 | 543 | 事实上,如果操作不是涉及 IO 的话,可能根本没有任何性能提升,而为什么涉及 IO 操作会提升性能,是怎么提升的,后面再讲解。 544 | 545 | 误区二:提升速度。 546 | 547 | 如果你一直使用 await 的话,那么是没法提升速度的,因为 await 会等待异步方法执行完毕,而且当前代码就会一直阻塞等待。如果去掉 await ,让多个方法并发执行的话,那么速度倒是会有所提升。但是要注意,并发之后会吃掉更多的 CPU 和内存。 548 | 549 | 550 | 551 | 我们首先来了解计算机底层的异步,并区分计算机底层异步和 .NET 异步的区别,来理解这两个误区到达错在哪里。 552 | 553 | 554 | 555 | 当涉及到网络、文件这些 IO 操作时,使用系统的异步 API ,可以提高性能。IO 是很广泛的称呼,包括**文件或 I/O 设备**的句柄 ,例如文件、文件流、物理磁盘、卷、控制台缓冲区、磁带驱动器、套接字、通信资源、mailslot 或管道。而 .NET 中很多地方封装了系统接口,然后在系统接口之上使用 C# 中的 Task 、async、await 屏蔽不同系统中关于异步的使用。要确保底层是使用了 IO ,否则使用了 Task、async、await 这些东西,也不会提高什么性能。 556 | 557 | > 后面再说为什么可以提升性能。 558 | 559 | 560 | 561 | 比如,在 Windows 中可以使用 ReadFileEx 函数异步读取文件。 562 | 563 | > 接口说明:[ReadFileEx 函数 (fileapi.h) - Win32 apps | Microsoft Learn](https://learn.microsoft.com/zh-cn/windows/win32/api/fileapi/nf-fileapi-readfileex) 564 | 565 | 566 | 567 | 下面是一个 Windows 下,使用 C++ 编写异步读取文件的代码示例: 568 | 569 | ```c++ 570 | #include 571 | #include 572 | 573 | const int BUFFER_SIZE = 1024; 574 | 575 | // 文件读取后的回调函数 576 | void ReadFileCompletionRoutine(DWORD dwError, DWORD dwBytesRead, LPOVERLAPPED lpOverlapped) 577 | { 578 | } 579 | 580 | int main() 581 | { 582 | const char* szFileName = "D:/a.txt"; // 文件名和路径 583 | HANDLE hFile = CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_OVERLAPPED, NULL); 584 | if (hFile == INVALID_HANDLE_VALUE) 585 | { 586 | std::cerr << "Failed to open file." << std::endl; 587 | return 1; 588 | } 589 | 590 | char* pBuffer = new char[BUFFER_SIZE + 1]; 591 | OVERLAPPED* pOverlapped = new OVERLAPPED; 592 | ZeroMemory(pOverlapped, sizeof(OVERLAPPED)); 593 | pOverlapped->hEvent = pBuffer; 594 | 595 | // 当读取文件完毕后,回调 ReadFileCompletionRoutine 596 | // 主线程不会阻塞 ReadFileEx 597 | if (!ReadFileEx(hFile, pBuffer, BUFFER_SIZE, pOverlapped, ReadFileCompletionRoutine)) 598 | { 599 | // 错误处理 600 | } 601 | std::cout << "执行"; 602 | // 模拟主线程在此处做一些其他工作 603 | Sleep(100000); 604 | 605 | CloseHandle(hFile); 606 | return 0; 607 | } 608 | ``` 609 | 610 | 611 | 612 | 核心代码是这一句: 613 | 614 | ```c++ 615 | ReadFileEx(hFile, pBuffer, BUFFER_SIZE, pOverlapped, ReadFileCompletionRoutine) 616 | ``` 617 | 618 | 619 | 620 | 在调用 `ReadFileEx` 函数后,操作系统会在**后台线程**中执行**文件读取操作**,当操作完成后,系统会调用指定的回调函数来处理结果。 `ReadFileEx` 函数不会阻塞线程的执行,所以使用了 ` Sleep(100000);` 阻塞当前线程的执行,确保回调事件可以发生。 621 | 622 | 你看看,这样是不是很麻烦,这里涉及线程同步的需求,我们如何判断回调事件已经发生,然后继续执行当前线程的代码? 623 | 624 | > 如果没有理解线程同步,请看看第二部分的章节。 625 | 626 | 627 | 628 | 使用 C#,就会变得简单很多: 629 | 630 | ```c# 631 | var value = await File.ReadAllTextAsync("a.txt"); 632 | ``` 633 | 634 | 635 | 636 | 637 | 638 | 接下来,说一下为什么 IO 操作会提升性能。 639 | 640 | 641 | 642 | 这里涉及到计算机组成原理。CPU 很快,而 CPU 跟内存的速度差了几个数量级,因此 CPU 每次执行指令都要从内存中取数据的话,会严重拖慢 CPU,因此出现了多级 CPU 缓存。 643 | 644 | 计算机中存在着 DMA 芯片,称为协处理器,它可以让内存与设备之间完成 IO 传输,而不需要 CPU 一直参与。 645 | 646 | 例如, CPU 执行指令从磁盘文件中加载文件到内存中,CPU 可以下方指令,然后 DMA 芯片控制内存跟 IO 设备传输数据,CPU 可以去执行其它任务,等 IO 操作完成,CPU 再回来执行接下来的指令。这一过程即 IO 异步。 647 | 648 | ![image-20220731165128240](images/image-20220731165128240.png) 649 | 650 | 651 | 652 | 简单来说,你的代码涉及到 IO 操作时,CPU 会下方指令让 IO 芯片跟 IO 设备工作,然后将你的代码挂起来,CPU 接着去做其它事情。当 IO 工作完成后,会通知 CPU ,CPU 就会接着执行你的代码。这样的话,IO 操作不需要消耗额外的 CPU。 653 | 654 | 655 | 656 | 在看编程资料时,我们往往会看到 IO模型、阻塞IO、非阻塞IO、同步IO、异步IO、零拷贝机制。是不是理解起来有点困难?看完就忘记了?感觉用不到的样子? 657 | 658 | 因为这些是跟硬件和系统内核打交道的, 如果不搞底层开发的话,其实也没必要深究,要面试的时候倒是可以背八股文。 659 | 660 | > 推荐阅读:零拷贝机制:https://www.whuanle.cn/archives/21051 661 | 662 | 663 | 664 | 如果你的代码不会使用到 IO 操作,那么你写的 `async/awaiit`,也只是实现 “异步代码”,不会带来性能上的提升。 665 | 666 | 667 | 668 | 在 C# 中,执行一个异步 IO 代码的示例如下: 669 | 670 | ```csharp 671 | static async Task Main() 672 | { 673 | var stream = File.OpenRead("D:/a.txt"); 674 | string content = await new StreamReader(stream).ReadToEndAsync(); 675 | } 676 | ``` 677 | 678 | 679 | 680 | 这个代码表示打开一个文件并读取文件内容到程序内存中。我们知道,CPU 速度比内存高了几个数量级,而内存又比磁盘搞了 N 个数量级。在内存读取磁盘文件字节到内存中时,需要等待较长时间,如果让 CPU 一直在等,那么会很浪费 CPU 。 681 | 682 | 而异步 IO 可以让内存跟跟磁盘独立工作,当执行到异步 IO 的代码时,CPU 将当前代码挂起,等内存和磁盘传输完毕, CPU 再回到代码继续执行下去。 683 | 684 | 685 | 686 | -------------------------------------------------------------------------------- /3.task/1.task1.md: -------------------------------------------------------------------------------- 1 | # 3.1 任务基础 1 2 | 3 | ## 多线程编程 4 | 5 | ### 多线程编程模式 6 | 7 | .NET 中,有三种异步编程模式,分别是基于任务的异步模式(TAP)、基于事件的异步模式(EAP)、异步编程模式(APM)。 8 | 9 | - **基于任务的异步模式 (TAP)** :.NET 推荐使用的异步编程方法,该模式使用单一方法表示异步操作的开始和完成。包括我们常用的 async 、await 关键字,属于该模式的支持。 10 | - 基于事件的异步模式 (EAP) :是提供异步行为的基于事件的旧模型。在线程池一章中提到过此模式,.NET Core 已经不支持。 11 | - 异步编程模型 (APM) 模式:也称为 [IAsyncResult](https://docs.microsoft.com/zh-cn/dotnet/api/system.iasyncresult) 模式,,这是使用 IAsyncResult 接口提供异步行为的旧模型。.NET Core 也不支持。 12 | 13 | 14 | 15 | 前面,我们学习了三部分的内容: 16 | 17 | * 线程基础:如何创建线程、获取线程信息以及等待线程完成任务; 18 | * 线程同步:探究各种方式实现进程和线程同步,以及线程等待; 19 | * 线程池:线程池的优点和使用方法,基于任务的操作; 20 | 21 | 这篇开始探究任务和异步,而任务和异步是十分复杂的,内容错综复杂,笔者可能讲不好。。。 22 | 23 | 24 | 25 | ### 探究优点 26 | 27 | 在前面中,学习多线程(线程基础和线程同步),一共写了 10 篇,写了这么多代码,我们现在来探究一下多线程编程的复杂性。 28 | 29 | 1. 传递数据和返回结果 30 | 31 | 传递数据倒是没啥问题,只是难以获取到线程的返回值,处理线程的异常也需要技巧。 32 | 33 | 2. 监控线程的状态 34 | 35 | 新建新的线程后,如果需要确定新线程在何时完成,需要自旋或阻塞等方式等待。 36 | 37 | 3. 线程安全 38 | 39 | 设计时要考虑如果避免死锁、合理使用各种同步锁,要考虑原子操作,同步信号的处理需要技巧。 40 | 41 | 4. 性能 42 | 43 | 玩多线程,最大需求就是提升性能,但是多线程中有很多坑,使用不当反而影响性能。 44 | 45 |
[以上总结可参考《C# 7.0本质论》19.3节,《C# 7.0核心技术指南》14.3 节]
46 | 我们通过使用线程池,可以解决上面的部分问题,但是还有更加好的选择,就是 Task(任务)。另外 Task 也是异步编程的基础类型,后面很多内容要围绕 Task 展开。 47 | 48 | 原理的东西,还是多参考微软官方文档和书籍,笔者讲的不一定准确,而且不会深入说明这些。 49 | 50 | 51 | 52 | ## 任务操作 53 | 54 | 任务(Task)实在太多 API 了,也有各种骚操作,要讲清楚实在不容易,我们要慢慢来,一点点进步,一点点深入,多写代码测试。 55 | 56 | 下面与笔者一起,一步步熟悉、摸索 Task 的 API。 57 | 58 | 59 | 60 | ### 两种创建任务的方式 61 | 62 | 通过其构造函数创建一个任务,其构造函数定义为: 63 | 64 | ```csharp 65 | public Task (Action action); 66 | ``` 67 | 68 | 其示例如下: 69 | 70 | ```csharp 71 | class Program 72 | { 73 | static void Main() 74 | { 75 | // 定义两个任务 76 | Task task1 = new Task(()=> 77 | { 78 | Console.WriteLine("① 开始执行"); 79 | Thread.Sleep(TimeSpan.FromSeconds(1)); 80 | 81 | Console.WriteLine("① 执行中"); 82 | Thread.Sleep(TimeSpan.FromSeconds(1)); 83 | 84 | Console.WriteLine("① 执行即将结束"); 85 | }); 86 | 87 | Task task2 = new Task(MyTask); 88 | // 开始任务 89 | task1.Start(); 90 | task2.Start(); 91 | 92 | Console.ReadKey(); 93 | } 94 | 95 | private static void MyTask() 96 | { 97 | Console.WriteLine("② 开始执行"); 98 | Thread.Sleep(TimeSpan.FromSeconds(1)); 99 | 100 | Console.WriteLine("② 执行中"); 101 | Thread.Sleep(TimeSpan.FromSeconds(1)); 102 | 103 | Console.WriteLine("② 执行即将结束"); 104 | } 105 | } 106 | ``` 107 | 108 | `.Start()` 方法用于启动一个任务。微软文档解释:启动 Task,并将它安排到当前的 TaskScheduler 中执行。 109 | 110 | TaskScheduler 这个东西,我们后面讲,别急。 111 | 112 | 113 | 114 | 另一种方式则使用 `Task.Factory`,此属性用于创建和配置 `Task` 和 `Task` 实例的工厂方法。 115 | 116 | 使用https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--可以添加任务。 117 | 118 |

119 |

120 | 当需要对长时间运行、计算限制的任务(计算密集型)进行精细控制时才使用 StartNew() 方法;
121 | 官方推荐使用 Task.Run 方法启动计算限制任务。 122 |
123 | Task.Factory.StartNew() 可以实现比 Task.Run() 更细粒度的控制。 124 |
125 |

126 | 127 | `Task.Factory.StartNew()` 的重载方法是真的多,你可以参考: [https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskfactory.startnew?view=netcore-3.1#--) 128 | 129 | 这里我们使用两个重载方法编写示例: 130 | 131 | ```csharp 132 | public Task StartNew(Action action); 133 | ``` 134 | 135 | ```csharp 136 | public Task StartNew(Action action, TaskCreationOptions creationOptions); 137 | ``` 138 | 139 | 代码示例如下: 140 | 141 | ```csharp 142 | class Program 143 | { 144 | static void Main() 145 | { 146 | // 重载方法 1 147 | Task.Factory.StartNew(() => 148 | { 149 | Console.WriteLine("① 开始执行"); 150 | Thread.Sleep(TimeSpan.FromSeconds(1)); 151 | 152 | Console.WriteLine("① 执行中"); 153 | Thread.Sleep(TimeSpan.FromSeconds(1)); 154 | 155 | Console.WriteLine("① 执行即将结束"); 156 | }); 157 | 158 | // 重载方法 1 159 | Task.Factory.StartNew(MyTask); 160 | 161 | // 重载方法 2 162 | Task.Factory.StartNew(() => 163 | { 164 | Console.WriteLine("① 开始执行"); 165 | Thread.Sleep(TimeSpan.FromSeconds(1)); 166 | 167 | Console.WriteLine("① 执行中"); 168 | Thread.Sleep(TimeSpan.FromSeconds(1)); 169 | 170 | Console.WriteLine("① 执行即将结束"); 171 | },TaskCreationOptions.LongRunning); 172 | 173 | Console.ReadKey(); 174 | } 175 | 176 | // public delegate void TimerCallback(object? state); 177 | private static void MyTask() 178 | { 179 | Console.WriteLine("② 开始执行"); 180 | Thread.Sleep(TimeSpan.FromSeconds(1)); 181 | 182 | Console.WriteLine("② 执行中"); 183 | Thread.Sleep(TimeSpan.FromSeconds(1)); 184 | 185 | Console.WriteLine("② 执行即将结束"); 186 | } 187 | } 188 | ``` 189 | 190 | 通过 `Task.Factory.StartNew()` 方法添加的任务,会进入线程池任务队列然后自动执行,不需要手动启动。 191 | 192 | `TaskCreationOptions.LongRunning` 是控制任务创建特性的枚举,后面讲。 193 | 194 | 195 | 196 | ### Task.Run() 创建任务 197 | 198 | `Task.Run()` 创建任务,跟 `Task.Factory.StartNew()` 差不多,当然 `Task.Run()` 还有很多重载方法和骚操作,我们后面再来学。 199 | 200 | `Task.Run()` 创建任务示例代码如下: 201 | 202 | ```csharp 203 | static void Main() 204 | { 205 | Task.Run(() => 206 | { 207 | Console.WriteLine("① 开始执行"); 208 | Thread.Sleep(TimeSpan.FromSeconds(1)); 209 | 210 | Console.WriteLine("① 执行中"); 211 | Thread.Sleep(TimeSpan.FromSeconds(1)); 212 | 213 | Console.WriteLine("① 执行即将结束"); 214 | }); 215 | Console.ReadKey(); 216 | } 217 | ``` 218 | 219 | 220 | 221 | 222 | 223 | ### 取消任务 224 | 225 | 取消任务,[《C#多线程(12):线程池》](https://www.cnblogs.com/whuanle/p/12787505.html#任务取消功能) 中说过一次,不过控制太自由,全靠任务本身自觉判断是否取消。 226 | 227 | 这里我们通过 Task 来实现任务的取消,其取消是实时的、自动的,并且不需要手工控制。 228 | 229 | 其构造函数如下: 230 | 231 | ```csharp 232 | public Task StartNew(Action action, CancellationToken cancellationToken); 233 | ``` 234 | 235 | 代码示例如下: 236 | 237 | 按下回车键的时候记得切换字母模式。 238 | 239 | ```csharp 240 | class Program 241 | { 242 | static void Main() 243 | { 244 | Console.WriteLine("任务开始启动,按下任意键,取消执行任务"); 245 | CancellationTokenSource cts = new CancellationTokenSource(); 246 | Task.Factory.StartNew(MyTask, cts.Token); 247 | 248 | Console.ReadKey(); 249 | 250 | cts.Cancel(); // 取消任务 251 | Console.ReadKey(); 252 | } 253 | 254 | // public delegate void TimerCallback(object? state); 255 | private static void MyTask() 256 | { 257 | Console.WriteLine(" 开始执行"); 258 | int i = 0; 259 | while (true) 260 | { 261 | Console.WriteLine($" 第{i}次任务"); 262 | Thread.Sleep(TimeSpan.FromSeconds(1)); 263 | 264 | Console.WriteLine(" 执行中"); 265 | Thread.Sleep(TimeSpan.FromSeconds(1)); 266 | 267 | Console.WriteLine(" 执行结束"); 268 | i++; 269 | } 270 | } 271 | } 272 | ``` 273 | 274 | 275 | 276 | ### 父子任务 277 | 278 | 前面创建任务的时候,我们碰到了 `TaskCreationOptions.LongRunning` 这个枚举类型,这个枚举用于控制任务的创建以及设定任务的行为。 279 | 280 | 其枚举如下: 281 | 282 | | 枚举 | 值 | 说明 | 283 | | ------------------------------ | ---- | ------------------------------------------------------------ | 284 | | AttachedToParent | 4 | 指定将任务附加到任务层次结构中的某个父级。 | 285 | | DenyChildAttach | 8 | 指定任何尝试作为附加的子任务执行的子任务都无法附加到父任务,会改成作为分离的子任务执行。 | 286 | | HideScheduler | 16 | 防止环境计划程序被视为已创建任务的当前计划程序。 | 287 | | LongRunning | 2 | 指定任务将是长时间运行的、粗粒度的操作,涉及比细化的系统更少、更大的组件。 | 288 | | None | 0 | 指定应使用默认行为。 | 289 | | PreferFairness | 1 | 提示 TaskScheduler 以一种尽可能公平的方式安排任务。 | 290 | | RunContinuationsAsynchronously | 64 | 强制异步执行添加到当前任务的延续任务。 | 291 | 292 | 这个枚举在 `TaskFactory` 和 `TaskFactory` 、`Task` 和 `Task` 、 293 | 294 | `StartNew()`、`FromAsync()` 、`TaskCompletionSource` 等地方可以使用到。 295 | 296 |

297 |

298 | 注意,子任务使用了 TaskCreationOptions.AttachedToParent ,并不是指父任务要等待子任务完成后,父任务才能继续完往下执行;而是指父任务如果先执行完毕,那么必须等待子任务完成后,父任务才算完成。 299 |
300 |

301 | 302 | 303 | 304 | 305 | 306 | 这里来探究 `TaskCreationOptions.AttachedToParent`的使用。代码示例如下: 307 | 308 | ```csharp 309 | // 父子任务 310 | Task task = new Task(() => 311 | { 312 | // TaskCreationOptions.AttachedToParent 313 | // 将此任务附加到父任务中 314 | // 父任务需要等待所有子任务完成后,才能算完成 315 | Task task1 = new Task(() => 316 | { 317 | Thread.Sleep(TimeSpan.FromSeconds(1)); 318 | for (int i = 0; i < 5; i++) 319 | { 320 | Console.WriteLine(" 内层任务1"); 321 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 322 | } 323 | }, TaskCreationOptions.AttachedToParent); 324 | task1.Start(); 325 | 326 | Console.WriteLine("最外层任务"); 327 | Thread.Sleep(TimeSpan.FromSeconds(1)); 328 | }); 329 | 330 | task.Start(); 331 | task.Wait(); 332 | 333 | Console.ReadKey(); 334 | ``` 335 | 336 | 而 `TaskCreationOptions.DenyChildAttach` 则不允许其它任务附加到外层任务中。 337 | 338 | ```csharp 339 | static void Main() 340 | { 341 | // 不允许出现父子任务 342 | Task task = new Task(() => 343 | { 344 | Task task1 = new Task(() => 345 | { 346 | Thread.Sleep(TimeSpan.FromSeconds(1)); 347 | for (int i = 0; i < 5; i++) 348 | { 349 | Console.WriteLine(" 内层任务1"); 350 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 351 | } 352 | }, TaskCreationOptions.AttachedToParent); 353 | task1.Start(); 354 | 355 | Console.WriteLine("最外层任务"); 356 | Thread.Sleep(TimeSpan.FromSeconds(1)); 357 | }, TaskCreationOptions.DenyChildAttach); // 不收儿子 358 | 359 | task.Start(); 360 | task.Wait(); 361 | 362 | Console.ReadKey(); 363 | } 364 | ``` 365 | 366 | 然后,这里也学习了一个新的 Task 方法:`Wait()` 等待 Task 完成执行过程。`Wait()` 也可以设置超时时间。 367 | 368 | 如果父任务是通过调用 Task.Run 方法而创建的,则可以隐式阻止子任务附加到其中。 369 | 370 | 关于附加的子任务,请参考:[https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1](https://docs.microsoft.com/zh-cn/dotnet/standard/parallel-programming/attached-and-detached-child-tasks?view=netcore-3.1) 371 | 372 | 373 | 374 | ### 任务返回结果以及异步获取返回结果 375 | 376 | 要获取任务返回结果,要使用泛型类或方法创建任务,例如 `Task`、`Task.Factory.StartNew()`、`Task.Run`。 377 | 378 | 通过 其泛型的 的 `Result` 属性,可以获得返回结果。 379 | 380 | 异步获取任务执行结果: 381 | 382 | ```csharp 383 | class Program 384 | { 385 | static void Main() 386 | { 387 | // ******************************* 388 | Task task = new Task(() => 389 | { 390 | return 666; 391 | }); 392 | // 执行 393 | task.Start(); 394 | // 获取结果,属于异步 395 | int number = task.Result; 396 | 397 | // ******************************* 398 | task = Task.Factory.StartNew(() => 399 | { 400 | return 666; 401 | }); 402 | 403 | // 也可以异步获取结果 404 | number = task.Result; 405 | 406 | // ******************************* 407 | task = Task.Run(() => 408 | { 409 | return 666; 410 | }); 411 | 412 | // 也可以异步获取结果 413 | number = task.Result; 414 | Console.ReadKey(); 415 | } 416 | } 417 | ``` 418 | 419 | 如果要同步的话,可以改成: 420 | 421 | ```csharp 422 | int number = Task.Factory.StartNew(() => 423 | { 424 | return 666; 425 | }).Result; 426 | ``` 427 | 428 | 429 | 430 | ### 捕获任务异常 431 | 432 | 进行中的任务发生了异常,不会直接抛出来阻止主线程执行,当获取任务处理结果或者等待任务完成时,异常会重新抛出。 433 | 434 | 示例如下: 435 | 436 | ```csharp 437 | static void Main() 438 | { 439 | // ******************************* 440 | Task task = new Task(() => 441 | { 442 | throw new Exception("反正就想弹出一个异常"); 443 | }); 444 | // 执行 445 | task.Start(); 446 | Console.WriteLine("任务中的异常不会直接传播到主线程"); 447 | Thread.Sleep(TimeSpan.FromSeconds(1)); 448 | 449 | // 当任务发生异常,获取结果时会弹出 450 | int number = task.Result; 451 | 452 | // task.Wait(); 等待任务时,如果发生异常,也会弹出 453 | 454 | Console.ReadKey(); 455 | } 456 | ``` 457 | 458 | 乱抛出异常不是很好的行为噢~可以改成如下: 459 | 460 | ```csharp 461 | static void Main() 462 | { 463 | Task task = new Task(() => 464 | { 465 | try 466 | { 467 | throw new Exception("反正就想弹出一个异常"); 468 | return new Program(); 469 | } 470 | catch 471 | { 472 | return null; 473 | } 474 | }); 475 | task.Start(); 476 | 477 | var result = task.Result; 478 | if (result is null) 479 | Console.WriteLine("任务执行失败"); 480 | else Console.WriteLine("任务执行成功"); 481 | 482 | Console.ReadKey(); 483 | } 484 | ``` 485 | 486 | 487 | 488 | 489 | 490 | ### 全局捕获任务异常 491 | 492 | `TaskScheduler.UnobservedTaskException` 是一个事件,其委托定义如下: 493 | 494 | ```csharp 495 | public delegate void EventHandler(object? sender, TEventArgs e); 496 | ``` 497 | 498 | 下面是一个示例: 499 | 500 | 请发布程序后,打开目录执行程序。 501 | 502 | ```csharp 503 | class Program 504 | { 505 | static void Main() 506 | { 507 | TaskScheduler.UnobservedTaskException += MyTaskException; 508 | 509 | Task.Factory.StartNew(() => 510 | { 511 | throw new ArgumentNullException(); 512 | }); 513 | Thread.Sleep(100); 514 | GC.Collect(); 515 | GC.WaitForPendingFinalizers(); 516 | 517 | Console.WriteLine("Done"); 518 | Console.ReadKey(); 519 | } 520 | public static void MyTaskException(object sender, UnobservedTaskExceptionEventArgs eventArgs) 521 | { 522 | // eventArgs.SetObserved(); 523 | ((AggregateException)eventArgs.Exception).Handle(ex => 524 | { 525 | Console.WriteLine("Exception type: {0}", ex.GetType()); 526 | return true; 527 | }); 528 | } 529 | } 530 | ``` 531 | 532 | `TaskScheduler.UnobservedTaskException` 到底怎么用,笔者不太清楚,效果难以观察,读者可自行参考: 533 | 534 | [https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException](https://stackoverflow.com/search?q=TaskScheduler.UnobservedTaskException) -------------------------------------------------------------------------------- /3.task/3.task3.md: -------------------------------------------------------------------------------- 1 | # 3.3 任务基础 3 2 | 3 | 任务基础一共三篇,本篇是第三篇,之后开始学习异步编程、并发、异步I/O的知识。 4 | 5 | 本篇会继续讲述 Task 的一些 API 和常用的操作。 6 | 7 | ### TaskAwaiter 8 | 9 | 先说一下 `TaskAwaiter`,`TaskAwaiter` 表示等待异步任务完成的对象并为结果提供参数。 10 | 11 | Task 有个 `GetAwaiter()` 方法,会返回`TaskAwaiter` 或`TaskAwaiter`,`TaskAwaiter` 类型在 `System.Runtime.CompilerServices` 命名空间中定义。 12 | 13 | `TaskAwaiter` 类型的属性和方法如下: 14 | 15 | 属性: 16 | 17 | | 属性 | 说明 | 18 | | ----------- | ---------------------------------------- | 19 | | IsCompleted | 获取一个值,该值指示异步任务是否已完成。 | 20 | 21 | 方法: 22 | 23 | | 方法 | 说明 | 24 | | ------------------------- | ----------------------------------------------------------- | 25 | | GetResult() | 结束异步任务完成的等待。 | 26 | | OnCompleted(Action) | 将操作设置为当 TaskAwaiter 对象停止等待异步任务完成时执行。 | 27 | | UnsafeOnCompleted(Action) | 计划与此 awaiter 相关异步任务的延续操作。 | 28 | 29 | 使用示例如下: 30 | 31 | ```csharp 32 | static void Main() 33 | { 34 | Task task = new Task(()=> 35 | { 36 | Console.WriteLine("我是前驱任务"); 37 | Thread.Sleep(TimeSpan.FromSeconds(1)); 38 | return 666; 39 | }); 40 | 41 | TaskAwaiter awaiter = task.GetAwaiter(); 42 | 43 | awaiter.OnCompleted(()=> 44 | { 45 | Console.WriteLine("前驱任务完成时,我就会继续执行"); 46 | }); 47 | task.Start(); 48 | 49 | Console.ReadKey(); 50 | } 51 | ``` 52 | 53 | 另外,我们前面提到过,任务发生未经处理的异常,任务被终止,也算完成任务。 54 | 55 | ### 延续的另一种方法 56 | 57 | 上一节我们介绍了 `.ContinueWith()` 方法来实现延续,这里我们介绍另一个延续方法 `.ConfigureAwait()`。 58 | 59 | `.ConfigureAwait()` 如果要尝试将延续任务封送回原始上下文,则为 `true`;否则为 `false`。 60 | 61 | 我来解释一下, `.ContinueWith()` 延续的任务,当前驱任务完成后,延续任务会继续在此线程上继续执行。这种方式是同步的,前者和后者连续在一个线程上运行。 62 | 63 | ` .ConfigureAwait(false)` 方法可以实现异步,前驱方法完成后,可以不理会后续任务,而且后续任务可以在任意一个线程上运行。这个特性在 UI 界面程序上特别有用。 64 | 65 | 可以参考:[https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f](https://medium.com/bynder-tech/c-why-you-should-use-configureawait-false-in-your-library-code-d7837dce3d7f) 66 | 67 | 其使用方法如下: 68 | 69 | ```csharp 70 | static void Main() 71 | { 72 | Task task = new Task(()=> 73 | { 74 | Console.WriteLine("我是前驱任务"); 75 | Thread.Sleep(TimeSpan.FromSeconds(1)); 76 | return 666; 77 | }); 78 | 79 | ConfiguredTaskAwaitable.ConfiguredTaskAwaiter awaiter = task.ConfigureAwait(false).GetAwaiter(); 80 | 81 | awaiter.OnCompleted(()=> 82 | { 83 | Console.WriteLine("前驱任务完成时,我就会继续执行"); 84 | }); 85 | task.Start(); 86 | 87 | Console.ReadKey(); 88 | } 89 | ``` 90 | 91 | `ConfiguredTaskAwaitable.ConfiguredTaskAwaiter ` 拥有跟 `TaskAwaiter` 一样的属性和方法。 92 | 93 | `.ContinueWith()` 跟 ` .ConfigureAwait(false)` 还有一个区别就是 前者可以延续多个任务和延续任务的任务(多层)。后者只能延续一层任务(一层可以有多个任务)。 94 | 95 | 96 | 97 | ### 另一种创建任务的方法 98 | 99 | 前面提到提到过,创建任务的三种方法:`new Task()`、`Task.Run()`、`Task.Factory.SatrtNew()`,现在来学习第四种方法:`TaskCompletionSource` 类型。 100 | 101 | 我们来看看 `TaskCompletionSource` 类型的属性和方法: 102 | 103 | 属性: 104 | 105 | | 属性 | 说明 | 106 | | ---- | ------------------------------------------- | 107 | | Task | 获取由此 Task 创建的 TaskCompletionSource。 | 108 | 109 | 方法: 110 | 111 | | 方法 | 说明 | 112 | | --------------------------------- | ------------------------------------------------------------ | 113 | | SetCanceled() | 将基础 Task 转换为 Canceled 状态。 | 114 | | SetException(Exception) | 将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。 | 115 | | SetException(IEnumerable) | 将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。 | 116 | | SetResult(TResult) | 将基础 Task 转换为 RanToCompletion 状态。 | 117 | | TrySetCanceled() | 尝试将基础 Task 转换为 Canceled 状态。 | 118 | | TrySetCanceled(CancellationToken) | 尝试将基础 Task 转换为 Canceled 状态并启用要存储在取消的任务中的取消标记。 | 119 | | TrySetException(Exception) | 尝试将基础 Task 转换为 Faulted 状态,并将其绑定到一个指定异常上。 | 120 | | TrySetException(IEnumerable) | 尝试将基础 Task 转换为 Faulted 状态,并对其绑定一些异常对象。 | 121 | | TrySetResult(TResult) | 尝试将基础 Task 转换为 RanToCompletion 状态。 | 122 | 123 | `TaskCompletionSource` 类可以对任务的生命周期做控制。 124 | 125 | 首先要通过 `.Task` 属性,获得一个 `Task` 或 `Task` 。 126 | 127 | ```csharp 128 | TaskCompletionSource task = new TaskCompletionSource(); 129 | Task myTask = task.Task; // Task myTask = task.Task; 130 | ``` 131 | 132 | 然后通过 `task.xxx()` 方法来控制 `myTask` 的生命周期,但是呢,myTask 本身是没有任务内容的。 133 | 134 | 使用示例如下: 135 | 136 | ```csharp 137 | static void Main() 138 | { 139 | TaskCompletionSource task = new TaskCompletionSource(); 140 | Task myTask = task.Task; // task 控制 myTask 141 | 142 | // 新开一个任务做实验 143 | Task mainTask = new Task(() => 144 | { 145 | Console.WriteLine("我可以控制 myTask 任务"); 146 | Console.WriteLine("按下任意键,我让 myTask 任务立即完成"); 147 | Console.ReadKey(); 148 | task.SetResult(666); 149 | }); 150 | mainTask.Start(); 151 | 152 | Console.WriteLine("开始等待 myTask 返回结果"); 153 | Console.WriteLine(myTask.Result); 154 | Console.WriteLine("结束"); 155 | Console.ReadKey(); 156 | } 157 | ``` 158 | 159 | 其它例如 `SetException(Exception)` 等方法,可以自行探索,这里就不再赘述。 160 | 161 | 参考资料:[https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/](https://devblogs.microsoft.com/premier-developer/the-danger-of-taskcompletionsourcet-class/) 162 | 163 | 这篇文章讲得不错,而且有图:[https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/](https://gigi.nullneuron.net/gigilabs/taskcompletionsource-by-example/) 164 | 165 | 166 | 167 | ### 实现一个支持同步和异步任务的类型 168 | 169 | 这部分内容对 `TaskCompletionSource` 继续进行讲解。 170 | 171 | 这里我们来设计一个类似 Task 类型的类,支持同步和异步任务。 172 | 173 | * 用户可以使用 `GetResult()` 同步获取结果; 174 | * 用户可以使用 `RunAsync()` 执行任务,使用 `.Result` 属性异步获取结果; 175 | 176 | 其实现如下: 177 | 178 | ```csharp 179 | /// 180 | /// 实现同步任务和异步任务的类型 181 | /// 182 | /// 183 | public class MyTaskClass 184 | { 185 | private readonly TaskCompletionSource source = new TaskCompletionSource(); 186 | private Task task; 187 | // 保存用户需要执行的任务 188 | private Func _func; 189 | 190 | // 是否已经执行完成,同步或异步执行都行 191 | private bool isCompleted = false; 192 | // 任务执行结果 193 | private TResult _result; 194 | 195 | /// 196 | /// 获取执行结果 197 | /// 198 | public TResult Result 199 | { 200 | get 201 | { 202 | if (isCompleted) 203 | return _result; 204 | else return task.Result; 205 | } 206 | } 207 | public MyTaskClass(Func func) 208 | { 209 | _func = func; 210 | task = source.Task; 211 | } 212 | 213 | /// 214 | /// 同步方法获取结果 215 | /// 216 | /// 217 | public TResult GetResult() 218 | { 219 | _result = _func.Invoke(); 220 | isCompleted = true; 221 | return _result; 222 | } 223 | 224 | /// 225 | /// 异步执行任务 226 | /// 227 | public void RunAsync() 228 | { 229 | Task.Factory.StartNew(() => 230 | { 231 | source.SetResult(_func.Invoke()); 232 | isCompleted = true; 233 | }); 234 | } 235 | } 236 | ``` 237 | 我们在 Main 方法中,创建任务示例: 238 | 239 | ```csharp 240 | class Program 241 | { 242 | static void Main() 243 | { 244 | // 实例化任务类 245 | MyTaskClass myTask1 = new MyTaskClass(() => 246 | { 247 | Thread.Sleep(TimeSpan.FromSeconds(1)); 248 | return "www.whuanle.cn"; 249 | }); 250 | 251 | // 直接同步获取结果 252 | Console.WriteLine(myTask1.GetResult()); 253 | 254 | 255 | // 实例化任务类 256 | MyTaskClass myTask2 = new MyTaskClass(() => 257 | { 258 | Thread.Sleep(TimeSpan.FromSeconds(1)); 259 | return "www.whuanle.cn"; 260 | }); 261 | 262 | // 异步获取结果 263 | myTask2.RunAsync(); 264 | 265 | Console.WriteLine(myTask2.Result); 266 | 267 | 268 | Console.ReadKey(); 269 | } 270 | } 271 | ``` 272 | 273 | 274 | 275 | ### Task.FromCanceled() 276 | 277 | 微软文档解释:创建 Task,它因指定的取消标记进行的取消操作而完成。 278 | 279 | 这里笔者抄来了一个[示例](https://stackoverflow.com/questions/25510766/how-to-create-a-cancelled-task): 280 | 281 | ```csharp 282 | var token = new CancellationToken(true); 283 | Task task = Task.FromCanceled(token); 284 | Task genericTask = Task.FromCanceled(token); 285 | ``` 286 | 287 | 网上很多这样的示例,但是,这个东西到底用来干嘛的?new 就行了? 288 | 289 | 带着疑问我们来探究一下,来个示例: 290 | 291 | ```csharp 292 | public static Task Test() 293 | { 294 | CancellationTokenSource source = new CancellationTokenSource(); 295 | source.Cancel(); 296 | return Task.FromCanceled(source.Token); 297 | } 298 | static void Main() 299 | { 300 | var t = Test(); // 在此设置断点,监控变量 301 | Console.WriteLine(t.IsCanceled); 302 | } 303 | ``` 304 | 305 | `Task.FromCanceled()` 可以构造一个被取消的任务。我找了很久,没有找到很好的示例,如果一个任务在开始前就被取消,那么使用 `Task.FromCanceled()` 是很不错的。 306 | 307 | 这里有很多示例可以参考:[https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/](https://www.csharpcodi.com/csharp-examples/System.Threading.Tasks.Task.FromCanceled(System.Threading.CancellationToken)/) 308 | 309 | 310 | 311 | ### 如何在内部取消任务 312 | 313 | 之前我们讨论过,使用 `CancellationToken` 取消令牌传递参数,使任务取消。但是都是从外部传递的,这里来实现无需 `CancellationToken` 就能取消任务。 314 | 315 | 我们可以使用 `CancellationToken` 的 `ThrowIfCancellationRequested()` 方法抛出 `System.OperationCanceledException` 异常,然后终止任务,任务会变成取消状态,不过任务需要先传入一个令牌。 316 | 317 | 这里笔者来设计一个难一点的东西,一个可以按顺序执行多个任务的类。 318 | 319 | 示例如下: 320 | 321 | ```csharp 322 | /// 323 | /// 能够完成多个任务的异步类型 324 | /// 325 | public class MyTaskClass 326 | { 327 | private List _actions = new List(); 328 | private CancellationTokenSource _source = new CancellationTokenSource(); 329 | private CancellationTokenSource _sourceBak = new CancellationTokenSource(); 330 | private Task _task; 331 | 332 | /// 333 | /// 添加一个任务 334 | /// 335 | /// 336 | public void AddTask(Action action) 337 | { 338 | _actions.Add(action); 339 | } 340 | 341 | /// 342 | /// 开始执行任务 343 | /// 344 | /// 345 | public Task StartAsync() 346 | { 347 | // _ = new Task() 对本示例无效 348 | _task = Task.Factory.StartNew(() => 349 | { 350 | for (int i = 0; i < _actions.Count; i++) 351 | { 352 | int tmp = i; 353 | Console.WriteLine($"第 {tmp} 个任务"); 354 | if (_source.Token.IsCancellationRequested) 355 | { 356 | Console.ForegroundColor = ConsoleColor.Red; 357 | Console.WriteLine("任务已经被取消"); 358 | Console.ForegroundColor = ConsoleColor.White; 359 | _sourceBak.Cancel(); 360 | _sourceBak.Token.ThrowIfCancellationRequested(); 361 | } 362 | _actions[tmp].Invoke(); 363 | } 364 | },_sourceBak.Token); 365 | return _task; 366 | } 367 | 368 | /// 369 | /// 取消任务 370 | /// 371 | /// 372 | public Task Cancel() 373 | { 374 | _source.Cancel(); 375 | 376 | // 这里可以省去 377 | _task = Task.FromCanceled(_source.Token); 378 | return _task; 379 | } 380 | } 381 | ``` 382 | 383 | Main 方法中: 384 | 385 | ```csharp 386 | static void Main() 387 | { 388 | // 实例化任务类 389 | MyTaskClass myTask = new MyTaskClass(); 390 | 391 | for (int i = 0; i < 10; i++) 392 | { 393 | int tmp = i; 394 | myTask.AddTask(() => 395 | { 396 | Console.WriteLine(" 任务 1 Start"); 397 | Thread.Sleep(TimeSpan.FromSeconds(1)); 398 | Console.WriteLine(" 任务 1 End"); 399 | Thread.Sleep(TimeSpan.FromSeconds(1)); 400 | }); 401 | } 402 | 403 | // 相当于 Task.WhenAll() 404 | Task task = myTask.StartAsync(); 405 | Thread.Sleep(TimeSpan.FromSeconds(1)); 406 | Console.WriteLine($"任务是否被取消:{task.IsCanceled}"); 407 | 408 | // 取消任务 409 | Console.ForegroundColor = ConsoleColor.Red; 410 | Console.WriteLine("按下任意键可以取消任务"); 411 | Console.ForegroundColor = ConsoleColor.White; 412 | Console.ReadKey(); 413 | 414 | var t = myTask.Cancel(); // 取消任务 415 | Thread.Sleep(TimeSpan.FromSeconds(2)); 416 | Console.WriteLine($"任务是否被取消:【{task.IsCanceled}】"); 417 | 418 | Console.ReadKey(); 419 | } 420 | ``` 421 | 422 | 你可以在任一阶段取消任务。 423 | 424 | 425 | 426 | ### yield 关键字 427 | 428 | 迭代器关键字,使得数据不需要一次性返回,可以在需要的时候一条条迭代。 429 | 430 | 迭代器方法运行到 `yield return` 语句时,会返回一个 `expression`,并保留当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。 431 | 432 | 可以使用 `yield break` 语句来终止迭代。 433 | 434 | 官方文档:[https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield](https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/keywords/yield) 435 | 436 | 网上的示例大多数都是 `foreach` 的,有些同学不理解这个到底是啥意思。笔者这里简单说明一下。 437 | 438 | 我们也可以这样写一个示例: 439 | 440 | 这里已经没有 `foreach` 了。 441 | 442 | ```csharp 443 | private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 444 | 445 | private static IEnumerable ForAsync() 446 | { 447 | int i = 0; 448 | while (i < list.Length) 449 | { 450 | i++; 451 | yield return list[i]; 452 | } 453 | } 454 | ``` 455 | 456 | 但是,同学又问,这个 return 返回的对象 要实现这个 `IEnumerable` 才行嘛?那些文档说到什么迭代器接口什么的,又是什么东西呢? 457 | 458 | 我们可以先来改一下示例: 459 | 460 | ```csharp 461 | 462 | private static int[] list = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; 463 | 464 | private static IEnumerable ForAsync() 465 | { 466 | int i = 0; 467 | while (i < list.Length) 468 | { 469 | int num = list[i]; 470 | i++; 471 | yield return num; 472 | } 473 | } 474 | ``` 475 | 476 | 你在 Main 方法中调用,看看是不是正常运行? 477 | 478 | ```csharp 479 | static void Main() 480 | { 481 | foreach (var item in ForAsync()) 482 | { 483 | Console.WriteLine(item); 484 | } 485 | Console.ReadKey(); 486 | } 487 | ``` 488 | 489 | 这样说明了,`yield return` 返回的对象,并不需要实现 `IEnumerable` 方法。 490 | 491 | 其实 `yield` 是语法糖关键字,你只要在循环中调用它就行了。 492 | 493 | ```csharp 494 | static void Main() 495 | { 496 | foreach (var item in ForAsync()) 497 | { 498 | Console.WriteLine(item); 499 | } 500 | Console.ReadKey(); 501 | } 502 | 503 | private static IEnumerable ForAsync() 504 | { 505 | int i = 0; 506 | while (i < 100) 507 | { 508 | i++; 509 | yield return i; 510 | } 511 | } 512 | } 513 | ``` 514 | 515 | 它会自动生成 `IEnumerable` ,而不需要你先实现 `IEnumerable` 。 516 | 517 | 518 | 519 | 520 | 521 | ### 补充知识点 522 | 523 | * 线程同步有多种方法:临界区(Critical Section)、互斥量(Mutex)、信号量(Semaphores)、事件(Event)、任务(Task); 524 | * `Task.Run()` 和 `Task.Factory.StartNew()` 封装了 Task; 525 | 526 | * `Task.Run()`是 `Task.Factory.StartNew()` 的简化形式; 527 | * 有些地方 `net Task()` 是无效的,因为你还需要 启动任务才行,不能光传递 Task;但是 `Task.Run()` 和 `Task.Factory.StartNew()` 可以; 528 | 529 | 530 | 531 | 532 | 533 | -------------------------------------------------------------------------------- /3.task/2.task2.md: -------------------------------------------------------------------------------- 1 | # 3.2 任务基础 2 2 | 3 | 上一篇,我们学习了任务的基础,学会多种方式场景任务和执行,异步获取返回结果等。上一篇讲述的知识比较多,这一篇只要是代码实践和示例操作。 4 | 5 | ### 判断任务状态 6 | 7 | | 属性 | 说明 | 8 | | ----------------------- | -------------------------------------------------- | 9 | | IsCanceled | 获取此 Task 实例是否由于被取消的原因而已完成执行。 | 10 | | IsCompleted | 获取一个值,它表示是否已完成任务。 | 11 | | IsCompletedSuccessfully | 了解任务是否运行到完成。 | 12 | | IsFaulted | 获取 Task是否由于未经处理异常的原因而完成。 | 13 | | Status | 获取此任务的 TaskStatus。 | 14 | 15 | 要检测一个任务是否出错(指任务因未经处理的异常而导致工作终止),要使用 `IsCanceled` 和 `IsFaulted` 两个属性,只要任务抛出异常,`IsFaulted` 为 true。但是取消任务本质是抛出 `OperationCancelExcetion` 异常,不代表任务出错。 16 | 17 | 即使任务抛出了未经处理的异常,也算是完成了任务,因此 `IsCompleted` 属性,会为 true。 18 | 19 | 示例如下: 20 | 21 | 代码有点多,不易观察,请复制到程序中运行。 22 | 23 | ```csharp 24 | class Program 25 | { 26 | static void Main() 27 | { 28 | // 正常任务 29 | Task task1 = new Task(() => 30 | { 31 | }); 32 | task1.Start(); 33 | Thread.Sleep(TimeSpan.FromSeconds(1)); 34 | GetResult(task1.IsCanceled, task1.IsFaulted); 35 | Console.WriteLine("任务是否完成:" + task1.IsCompleted); 36 | Console.WriteLine("-------------------"); 37 | 38 | // 异常任务 39 | Task task2 = new Task(() => 40 | { 41 | throw new Exception(); 42 | }); 43 | task2.Start(); 44 | Thread.Sleep(TimeSpan.FromSeconds(1)); 45 | GetResult(task2.IsCanceled, task2.IsFaulted); 46 | Console.WriteLine("任务是否完成:" + task2.IsCompleted); 47 | Console.WriteLine("-------------------"); 48 | Thread.Sleep(TimeSpan.FromSeconds(1)); 49 | 50 | CancellationTokenSource cts = new CancellationTokenSource(); 51 | // 取消任务 52 | Task task3 = new Task(() => 53 | { 54 | Thread.Sleep(TimeSpan.FromSeconds(3)); 55 | }, cts.Token); 56 | task3.Start(); 57 | cts.Cancel(); 58 | Thread.Sleep(TimeSpan.FromSeconds(1)); 59 | GetResult(task3.IsCanceled, task3.IsFaulted); 60 | Console.WriteLine("任务是否完成:" + task3.IsCompleted); 61 | Console.ReadKey(); 62 | } 63 | 64 | public static void GetResult(bool isCancel, bool isFault) 65 | { 66 | if (isCancel == false && isFault == false) 67 | Console.WriteLine("没有异常发生"); 68 | else if (isCancel == true) 69 | Console.WriteLine("任务被取消"); 70 | else 71 | Console.WriteLine("任务引发了未经处理的异常"); 72 | } 73 | } 74 | ``` 75 | 76 | 77 | 78 | ### 再说父子任务 79 | 80 | 在上一篇文章中《C#多线程:任务基础①》,我们学习了父子任务,父任务需要等待子任务完成后才算完成任务。 81 | 82 | 上一章只是给出示例,没有明确说明场景和实验结果,这里重新写一个示例来补充。 83 | 84 | 非父子任务: 85 | 86 | 外层任务不会等待内嵌的任务完成,直接完成或返回结果。 87 | 88 | ```csharp 89 | static void Main() 90 | { 91 | //两个任务没有从属关系,是独立的 92 | Task task = new Task(() => 93 | { 94 | // 非子任务 95 | Task task1 = new Task(() => 96 | { 97 | Thread.Sleep(TimeSpan.FromSeconds(1)); 98 | for (int i = 0; i < 5; i++) 99 | { 100 | Console.WriteLine(" 内层任务1"); 101 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 102 | } 103 | }); 104 | task1.Start(); 105 | return 666; 106 | }); 107 | task.Start(); 108 | Console.WriteLine($"任务运算结果是:{task.Result}"); 109 | Console.WriteLine("\n-------------------\n"); 110 | Console.ReadKey(); 111 | } 112 | ``` 113 | 114 | 父子任务: 115 | 116 | 父任务等待子任务完成后,才能算完成任务,然后返回结果。 117 | 118 | ```csharp 119 | static void Main() 120 | { 121 | // 父子任务 122 | Task task = new Task(() => 123 | { 124 | // 子任务 125 | Task task1 = new Task(() => 126 | { 127 | Thread.Sleep(TimeSpan.FromSeconds(1)); 128 | for (int i = 0; i < 5; i++) 129 | { 130 | Console.WriteLine(" 内层任务1"); 131 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 132 | } 133 | }, TaskCreationOptions.AttachedToParent); 134 | task1.Start(); 135 | 136 | Console.WriteLine("最外层任务"); 137 | return 666; 138 | }); 139 | 140 | task.Start(); 141 | Console.WriteLine($"任务运算结果是:{task.Result}"); 142 | Console.WriteLine("\n-------------------\n"); 143 | 144 | Console.ReadKey(); 145 | } 146 | ``` 147 | 148 | 149 | 150 | 151 | 152 | ### 组合任务/延续任务 153 | 154 | `Task.ContinueWith()` 方法创建一个在 任务(Task)实例 完成时异步执行的延续任务。 155 | 156 | `Task.ContinueWith()` 的重载方法非常多,可以参考:[https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.continuewith?view=netcore-3.1#--](https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.task.continuewith?view=netcore-3.1#--) 157 | 158 | 这里我们使用的构造函数定义如下: 159 | 160 | ```csharp 161 | public Task ContinueWith(Action continuationAction); 162 | ``` 163 | 164 | 一个简单的示例: 165 | 166 | ```csharp 167 | Task task = new Task(() => 168 | { 169 | Console.WriteLine(" 第一个任务"); 170 | Thread.Sleep(TimeSpan.FromSeconds(2)); 171 | }); 172 | 173 | // 接下来第二个任务 174 | task.ContinueWith(t => 175 | { 176 | Console.WriteLine($" 第二个任务}"); 177 | Thread.Sleep(TimeSpan.FromSeconds(2)); 178 | }); 179 | task.Start(); 180 | ``` 181 | 182 | ![file](images/image-1588063128308.png) 183 | 184 | 一个任务(Task) 是可以设置多个延续任务的,这些任务是并行的,例如: 185 | 186 | ```csharp 187 | static void Main() 188 | { 189 | Task task = new Task(() => 190 | { 191 | Console.WriteLine(" 第一个任务"); 192 | Thread.Sleep(TimeSpan.FromSeconds(1)); 193 | }); 194 | 195 | // 任务① 196 | task.ContinueWith(t => 197 | { 198 | for (int i = 0; i < 5; i++) 199 | { 200 | Console.WriteLine($" 任务① "); 201 | Thread.Sleep(TimeSpan.FromSeconds(1)); 202 | } 203 | }); 204 | 205 | // 任务② 206 | task.ContinueWith(t => 207 | { 208 | for (int i = 0; i < 5; i++) 209 | { 210 | Console.WriteLine($" 任务②"); 211 | Thread.Sleep(TimeSpan.FromSeconds(1)); 212 | } 213 | }); 214 | 215 | // 任务① 和 任务② 属于同级并行任务 216 | 217 | task.Start(); 218 | } 219 | ``` 220 | 221 | ![file](images/image-1588063628600.png) 222 | 223 | 通过多次实现延续/组合任务,会实现强有力的任务流程。 224 | 225 | 226 | 227 | ### 复杂的延续任务 228 | 229 | 经过上一小节,我们学习了 `ContinueWith()` 来延续任务,现在我们来学习更多的重载方法,实现更加复杂的延续。 230 | 231 | `ContinueWith()` 重载方法很多,它们的参数都含有下面几种参数之一或多个。 232 | 233 | - continuationAction 234 | 235 | 类型:Action 或 Func 236 | 237 | 一个要执行的任务。 238 | 239 | - state 240 | 241 | 类型:Object 242 | 243 | 给延续任务传递的参数。 244 | 245 | - cancellationToken 246 | 247 | 类型:CancellationToken 248 | 249 | 取消标记。 250 | 251 | - continuationOptions 252 | 253 | 类型:TaskContinuationOptions 254 | 255 | 控制延续任务的创建和特性。 256 | 257 | - scheduler 258 | 259 | 类型:TaskScheduler 260 | 261 | 要与延续任务关联并用于其执行过程的 TaskScheduler。 262 | 263 | 前面四个参数(类型),在以往的文章中已经出现过,这里就不再赘述;`TaskScheduler` 类型,这里先讲解,后面再说。 264 | 265 | 注意 `TaskCreationOptions` 和 `TaskContinuationOptions` 的区别,在前一篇我们学习过 `TaskCreationOptions`。这里来学习 `TaskContinuationOptions` 。 266 | 267 | `TaskContinuationOptions` 可以在以下重载上使用: 268 | 269 | ```csharp 270 | ContinueWith(Action, CancellationToken, TaskContinuationOptions, TaskScheduler) 271 | ``` 272 | 273 | ```csharp 274 | ContinueWith(Action>, TaskContinuationOptions 275 | ``` 276 | 277 | 278 | 279 | 在延续中,这样使用是无效的: 280 | 281 | ```csharp 282 | Task task = new Task(() => 283 | { 284 | Console.WriteLine(" 第一个任务"); 285 | Thread.Sleep(TimeSpan.FromSeconds(1)); 286 | }); 287 | task.ContinueWith(t => 288 | { 289 | for (int i = 0; i < 5; i++) 290 | { 291 | Console.WriteLine($" 任务① "); 292 | Thread.Sleep(TimeSpan.FromSeconds(1)); 293 | } 294 | },TaskContinuationOptions.AttachedToParent); 295 | ``` 296 | 297 | 因为 `TaskContinuationOptions` 需要有嵌套关系的父子任务,才能生效。 298 | 299 | 正确使用方法: 300 | 301 | ```csharp 302 | static void Main() 303 | { 304 | // 父子任务 305 | Task task = new Task(() => 306 | { 307 | // 子任务 308 | Task task1 = new Task(() => 309 | { 310 | Thread.Sleep(TimeSpan.FromSeconds(1)); 311 | Console.WriteLine(" 内层任务1"); 312 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 313 | }, TaskCreationOptions.AttachedToParent); 314 | 315 | task1.ContinueWith(t => 316 | { 317 | Thread.Sleep(TimeSpan.FromSeconds(1)); 318 | Console.WriteLine("内层延续任务,也属于子任务"); 319 | Thread.Sleep(TimeSpan.FromSeconds(0.5)); 320 | }, TaskContinuationOptions.AttachedToParent); 321 | 322 | task1.Start(); 323 | 324 | Console.WriteLine("最外层任务"); 325 | return 666; 326 | }); 327 | 328 | task.Start(); 329 | Console.WriteLine($"任务运算结果是:{task.Result}"); 330 | Console.WriteLine("\n-------------------\n"); 331 | 332 | Console.ReadKey(); 333 | } 334 | ``` 335 | 336 | ![file](images/image-1588071312510.png) 337 | 338 | ### 并行(异步)处理任务 339 | 340 | 这里我们来学习 `Task.WhenAll()` 方法的使用。 341 | 342 | `Task.WhenAll()` :等待提供的所有 Task 对象完成执行过程 343 | 344 | 使用示例如下: 345 | 346 | ```csharp 347 | static void Main() 348 | { 349 | List tasks = new List(); 350 | 351 | for (int i = 0; i < 5; i++) 352 | tasks.Add(Task.Run(() => 353 | { 354 | Console.WriteLine($"任务开始执行"); 355 | })); 356 | 357 | // public static Task WhenAll(IEnumerable tasks); 358 | 359 | // 相当于多个任务,生成一个任务 360 | Task taskOne = Task.WhenAll(tasks); 361 | // 不需要等待的话就去除 362 | taskOne.Wait(); 363 | 364 | Console.ReadKey(); 365 | } 366 | ``` 367 | 368 | ` Task taskOne = Task.WhenAll(tasks);` 可以写成 ` Task.WhenAll(tasks);`,返回的 Task 对象可以用来判断任务执行情况。 369 | 370 | 371 | 372 | 要注意,下面这样是无效的: 373 | 374 | 你可以修改上面的代码进行测试。 375 | 376 | ```csharp 377 | tasks.Add(new Task(() => 378 | { 379 | Console.WriteLine($"任务开始执行"); 380 | })); 381 | ``` 382 | 383 | 我也不知道为啥 `new Task()` 不行。。。 384 | 385 | 386 | 387 | 如果任务有返回值,则可以使用下面这种方法 388 | 389 | ```csharp 390 | static void Main() 391 | { 392 | List> tasks = new List>(); 393 | 394 | for (int i = 0; i < 5; i++) 395 | tasks.Add(Task.Run(() => 396 | { 397 | Console.WriteLine($"任务开始执行"); 398 | return new Random().Next(0,10); 399 | })); 400 | 401 | Task taskOne = Task.WhenAll(tasks); 402 | 403 | foreach (var item in taskOne.Result) 404 | Console.WriteLine(item); 405 | 406 | Console.ReadKey(); 407 | } 408 | ``` 409 | 410 | 411 | 412 | 413 | 414 | ### 并行(同步)处理任务 415 | 416 | `Task.WaitAll()`:等待提供的所有 Task 对象完成执行过程。 417 | 418 | 我们来看看 `Task.WaitAll()` 其中一个重载方法的定义: 419 | 420 | ```csharp 421 | public static bool WaitAll (Task[] tasks, int millisecondsTimeout, CancellationToken cancellationToken); 422 | ``` 423 | 424 | * tasks 类型:Task[] 425 | 426 | 要执行的所有任务。 427 | 428 | - millisecondsTimeout 任务:Int32 429 | 430 | 等待的毫秒数,-1 表示无限期等待。 431 | 432 | - cancellationToken 类型:CancellationToken 433 | 434 | 等待任务完成期间要观察的 CancellationToken。 435 | 436 | `Task.WaitAll()` 的示例如下: 437 | 438 | ```csharp 439 | static void Main() 440 | { 441 | List tasks = new List(); 442 | 443 | for (int i = 0; i < 5; i++) 444 | tasks.Add(Task.Run(() => 445 | { 446 | Console.WriteLine($"任务开始执行"); 447 | })); 448 | 449 | Task.WaitAll(tasks.ToArray()); 450 | 451 | Console.ReadKey(); 452 | } 453 | ``` 454 | 455 | `Task.WaitAll()` 会让当前线程等待所有任务执行完毕。并且 `Task.WaitAll()` 是没有泛型的,也么没有返回结果。 456 | 457 | ### 并行任务的 Task.WhenAny 458 | 459 | `Task.WhenAny()` 和 `Task.WhenAll()` 使用上差不多,`Task.WhenAll()` 当所有任务都完成时,才算完成,而 `Task.WhenAny()` 只要其中一个任务完成,都算完成。 460 | 461 | 这一点可以参考上面的 **父子任务**。 462 | 463 | 参考使用示例如下: 464 | 465 | ```csharp 466 | static void Main() 467 | { 468 | List tasks = new List(); 469 | 470 | for (int i = 0; i < 5; i++) 471 | tasks.Add(Task.Run(() => 472 | { 473 | Thread.Sleep(TimeSpan.FromSeconds(new Random().Next(0, 5))); 474 | Console.WriteLine(" 正在执行任务"); 475 | })); 476 | Task taskOne = Task.WhenAny(tasks); 477 | taskOne.Wait(); // 任意一个任务完成,就可以解除等待 478 | 479 | Console.WriteLine("有任务已经完成了"); 480 | 481 | Console.ReadKey(); 482 | } 483 | ``` 484 | 485 | 当然,`Task.WhenAny()` 也有泛型方法,可以返回结果。 486 | 487 | 488 | 489 | ### 并行任务状态 490 | 491 | `Task.Status` 属性可以获取任务的状态。其属性类型是一个 TaskStatus 枚举,其定义如下: 492 | 493 | | 枚举 | 值 | 说明 | 494 | | ---------------------------- | ---- | ------------------------------------------------------------ | 495 | | Canceled | 6 | 已经通过 CancellationToken 取消任务。 | 496 | | Created | 0 | 该任务已初始化,但尚未被计划。 | 497 | | Faulted | 7 | 由于未处理异常的原因而完成的任务。 | 498 | | RanToCompletion | 5 | 已成功完成执行的任务。 | 499 | | Running | 3 | 该任务正在运行,但尚未完成。 | 500 | | WaitingForActivation | 1 | 该任务正在等待 .NET Framework 基础结构在内部将其激活并进行计划。 | 501 | | WaitingForChildrenToComplete | 4 | 该任务已完成执行,正在隐式等待附加的子任务完成。 | 502 | | WaitingToRun | 2 | 该任务已被计划执行,但尚未开始执行。 | 503 | 504 | 505 | 506 | 在使用并行任务时,`Task.Status` 的值,有一定规律: 507 | 508 | * 如果有其中一个任务出现未经处理的异常,那么返回`TaskStatus.Faulted`; 509 | * 如果所有任务都出现未经处理的异常,会返回 `TaskStatus. RanToCompletion `; 510 | 511 | * 如果其中一个任务被取消(即使出现未经处理的异常),会返回 `TaskStaus.Canceled`; 512 | 513 | 514 | 515 | ### 循环中值变化问题 516 | 517 | 请运行测试下面两个示例: 518 | 519 | ```csharp 520 | static void Main() 521 | { 522 | for (int i = 0; i < 5; i++) 523 | new Thread(() => 524 | { 525 | Console.WriteLine($"i = {i}"); 526 | }).Start(); 527 | 528 | Console.ReadKey(); 529 | } 530 | ``` 531 | 532 | ```csharp 533 | static void Main() 534 | { 535 | List tasks = new List(); 536 | 537 | for (int i = 0; i < 5; i++) 538 | tasks.Add(Task.Run(() => 539 | { 540 | Console.WriteLine($"i = {i}"); 541 | })); 542 | Task taskOne = Task.WhenAll(tasks); 543 | taskOne.Wait(); 544 | 545 | Console.ReadKey(); 546 | } 547 | ``` 548 | 549 | 你会发现,两个示例的结果并不是 `1,2,3,4,5`,而是 `5,5,5,5,5`。 550 | 551 | 这个问题称为 Race condition(竞争条件),可以参考维基百科: 552 | 553 | [https://en.wikipedia.org/wiki/Race_condition](https://en.wikipedia.org/wiki/Race_condition) 554 | 555 | 微软文档里面也有关于此问题的说明,请参考: 556 | 557 | [https://docs.microsoft.com/zh-cn/archive/blogs/ericlippert/closing-over-the-loop-variable-considered-harmful](https://docs.microsoft.com/zh-cn/archive/blogs/ericlippert/closing-over-the-loop-variable-considered-harmful) 558 | 559 | 由于 i 在整个生命周期,内存都是在同一个位置,每个线程或任务对其值得使用,都是指向相同位置的。 560 | 561 | 这样就行了: 562 | 563 | ```csharp 564 | static void Main() 565 | { 566 | for (int i = 0; i < 5; i++) 567 | { 568 | int tmp = i; 569 | new Thread(() => 570 | { 571 | Console.WriteLine($"i = {tmp}"); 572 | }).Start(); 573 | } 574 | 575 | Console.ReadKey(); 576 | } 577 | ``` 578 | 579 | 这样是无效的: 580 | 581 | ```csharp 582 | for (int i = 0; i < 5; i++) 583 | new Thread(() => 584 | { 585 | int tmp = i; 586 | Console.WriteLine($"i = {tmp}"); 587 | }).Start(); 588 | ``` 589 | 590 | 591 | 592 | ### TaskScheduler 类 593 | 594 | 表示一个处理将任务排队到线程中的低级工作的对象。 595 | 596 | 在线程池一章中,提到过线程池线程有本地队列、线程偷窥等,我们关注到了线程池数量对提高吞吐量的影响,我们可以使用 TaskScheduler 实现自己的调度逻辑。 597 | 598 | TaskScheduler、ThreadPoolTaskScheduler 这些,笔者这里就不讲了,读者可以参考: 599 | 600 | https://docs.microsoft.com/zh-cn/dotnet/api/system.threading.tasks.taskscheduler?view=net-6.0 601 | --------------------------------------------------------------------------------