《C#并发编程经典实例》笔记
1.前言
最近趁着项目的一段平稳期研读了不少书籍,其中《C#并发编程经典实例》给我的印象还是比较深刻的。当然,这可能是由于近段日子看的书大多嘴炮大于实际,如《Head First设计模式》《Cracking the coding interview》等,所以陡然见到一本打着“实例”旗号的书籍,还是挺让我觉得耳目一新。本着分享和加深理解的目的,我特地整理了一些笔记(主要是Web开发中容易涉及的内容,所以部分章节如数据流,RX等我看了看就直接跳过了),以供审阅学习。语言和技术的魅力,真是不可捉摸
2.开宗明义
一直以来都有一种观点是实现底层架构,编写驱动和引擎,或者是框架和工具开发的才是高级开发人员,做上层应用的人仅仅是“码农”,其实能够利用好平台提供的相关类库,而不是全部采用底层技术自己实现,开发出高质量,稳定的应用程序,对技术能力的考验并不低于开发底层库,如TPL,async,await等。
3.开发原则和要点
(1)并发编程概述
- 并发:同时做多件事情
- 多线程:并发的一种形式,它采用多个线程来执行程序
- 并行处理:把正在执行的大量的任务分割成小块,分配给多个同时运行的线程
- 并行处理是多线程的一种,而多线程是并发的一种处理形式
- 异步编程:并发的一种形式,它采用future模式或者callback机制,以避免产生不必要的线程
- 异步编程的核心理念是异步操作:启动了的操作会在一段时间后完成。这个操作正在执行时,不会阻塞原来的线程。启动了这个操作的线程,可以继续执行其他任务。当操作完成后,会通知它的future,或者调用回调函数,以便让程序知道操作已经结束
- await关键字的作用:启动一个将会被执行的Task(该Task将在新线程中运行),并立即返回,所以await所在的函数不会被阻塞。当Task完成后,继续执行await后面的代码
- 响应式编程:并发的一种基于声明的编程方式,程序在该模式中对事件作出反应
- 不要用 void 作为 async 方法的返回类型! async 方法可以返回 void,但是这仅限于编写事件处理程序。一个普通的 async 方法如果没有返回值,要返回 Task,而不是 void
- async 方法在开始时以同步方式执行。在 async 方法内部,await 关键字对它的参数执行一个异步等待。它首先检查操作是否已经完成,如果完成了,就继续运行 (同步方式)。否则,它会暂停 async 方法,并返回,留下一个未完成的 task。一段时间后, 操作完成,async
方法就恢复运行。 - await代码中抛出异常后,异常会沿着Task方向前进到引用处
- 你一旦在代码中使用了异步,最好一直使用。调用 异步方法时,应该(在调用结束时)用 await 等待它返回的 task 对象。一定要避免使用 Task.Wait 或 Task.Result 方法,因为它们会导致死锁
- 线程是一个独立的运行单元,每个进程内部有多个线程,每个线程可以各自同时执行指令。 每个线程有自己独立的栈,但是与进程内的其他线程共享内存
- 每个.NET应用程序都维护着一个线程池,这种情况下,应用程序几乎不需要自行创建新的线程。你若要为 COM interop 程序创建 SAT 线程,就得 创建线程,这是唯一需要线程的情况
- 线程是低级别的抽象,线程池是稍微高级一点的抽象
- 并发编程用到的集合有两类:并发变成+不可变集合
- 大多数并发编程技术都有一个类似点:它们本质上都是函数式的。这里的函数式是作为一种基于函数组合的编程模式。函数式的一个编程原则是简洁(避免副作用),另一个是不变性(指一段数据不能被修改)
- .NET 4.0 引入了并行任务库(TPL),完全支持数据并行和任务并行。但是一些资源较少的 平台(例如手机),通常不支持 TPL。TPL 是 .NET 框架自带的
(2)异步编程基础
- 指数退避是一种重试策略,重试的延迟时间会逐 次增加。在访问 Web 服务时,最好的方式就是采用指数退避,它可以防止服务器被太多的重试阻塞
static async Task<string> DownloadStringWithRetries(string uri)
{
using (var client = new HttpClient())
{
// 第 1 次重试前等 1 秒,第 2 次等 2 秒,第 3 次等 4 秒。
var nextDelay = TimeSpan.FromSeconds(1);
for (int i = 0; i != 3; ++i)
{
try
{
return await client.GetStringAsync(uri);
}
catch
{ }
await Task.Delay(nextDelay);
nextDelay = nextDelay + nextDelay;
}
// 最后重试一次,以便让调用者知道出错信息。
return await client.GetStringAsync(uri);
}
}
- Task.Delay 适合用于对异步代码进行单元测试或者实现重试逻辑。要实现超时功能的话, 最好使用 CancellationToken
- 如何实现一个具有异步签名的同步方法。如果从异步接口或基类继承代码,但希望用同步的方法来实现它,就会出现这种情况。解决办法是可以使用 Task.FromResult 方法创建并返回一个新的 Task 对象,这个 Task 对象是已经 完成的,并有指定的值
- 使用 IProgress 和 Progress 类型。编写的 async 方法需要有 IProgress 参数,其 中 T 是需要报告的进度类型,可以展示操作的进度
- Task.WhenALl可以等待所有任务完成,而当每个Task抛出异常时,可以选择性捕获异常
- Task.WhenAny可以等待任一任务完成,使用它虽然可以完成超时任务(其中一个Task设为Task.Delay),但是显然用专门的带有取消标志的超时函数处理比较好
- 第一章提到async和上下文的问题:在默认情况下,一个 async 方法在被 await 调用后恢复运行时,会在原来的上下文中运行。而加上扩展方法ConfigureAwait(false)后,则会在await之后丢弃上下文
(3)并行开发的基础
- Parallel 类有一个简单的成员 Invoke,可用于需要并行调用一批方法,并且这些方法(大部分)是互相独立的
static void ProcessArray(double[] array)
{
Parallel.Invoke(
() => ProcessPartialArray(array, 0, array.Length / 2),
() => ProcessPartialArray(array, array.Length / 2,array.Length));
}
static void ProcessPartialArray(double[] array, int begin, int end)
{
// 计算密集型的处理过程 ...
}
- 在并发编程中,Task类有两个作用:作为并行任务,或作为异步任务。并行任务可以使用 阻塞的成员函数,例如 Task.Wait、Task.Result、Task.WaitAll 和 Task.WaitAny。并行任务通常也使用 AttachedToParent 来建立任务之间的“父 / 子”关系。并行任务的创建需要 用 Task.Run 或者 Task.Factory.StartNew。
- 相反的,异步任务应该避免使用阻塞的成员函数,而应该使用 await、Task.WhenAll 和 Task. WhenAny。异步任务不使用 AttachedToParent,但可以通过 await 另一个任务,建立一种隐 式的“父 / 子”关系。
(4)测试技巧
- MSTest从Visual Studio2012 版本开始支持 async Task 类型的单元测试
- 如果单元测试框架不支持 async Task 类型的单元测试,就需要做一些额外的修改才能等待异步操作。其中一种做法是使用 Task.Wait,并在有错误时拆开 AggregateException 对象。我的建议是使用 NuGet 包 Nito.AsyncEx 中的 AsyncContext 类
这里附上一个ABP中实现的可操作AsyncHelper类,就是基于AsyncContext实现
/// <summary>
/// Provides some helper methods to work with async methods.
/// </summary>
public static class AsyncHelper
{
/// <summary>
/// Checks if given method is an async method.
/// </summary>
/// <param name="method">A method to check</param>
public static bool IsAsyncMethod(MethodInfo method)
{
return (
method.ReturnType == typeof(Task) ||
(method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>))
);
}
/// <summary>
/// Runs a async method synchronously.
/// </summary>
/// <param name="func">A function that returns a result</param>
/// <typeparam name="TResult">Result type</typeparam>
/// <returns>Result of the async operation</returns>
public static TResult RunSync<TResult>(Func<Task<TResult>> func)
{
return AsyncContext.Run(func);
}
/// <summary>
/// Runs a async method synchronously.
/// </summary>
/// <param name="action">An async action</param>
public static void RunSync(Func<Task> action)
{
AsyncContext.Run(action);
}
}
- 在 async 代码中,关键准则之一就是避免使用 async void。我非常建议大家在对 async void 方法做单元测试时进行代码重构,而不是使用 AsyncContext。
(5)集合
线程安全集合是可同时被多个线程修改的可变集合。线程安全集合混合使用了细粒度锁定和无锁技术,以确保线程被阻塞的时间最短(通常情况下是根本不阻塞)。对很多线程安全集合进行枚举操作时,内部创建了该集合的一个快照(snapshot),并对这个快照进行枚举操作。线程安全集合的主要优点是多个线程可以安全地对其进行访问,而代码只会被阻塞很短的时间,或根本不阻塞
ConcurrentDictionary<TKey, TValue>是数据结构中的精品,它是线程安全的,混合使用了细粒度锁定和无锁技术,以确保绝大多数情况下能进行快速访问.
ConcurrentDictionary<TKey, TValue> 内置了AddOrUpdate, TryRemove, TryGetValue等方法。如果多个线程读写一个共享集合,使用ConcurrentDictionary<TKey, TValue>是最合适的,如果不会频繁修改,那就更适合使用ImmutableDictionary<TKey, TValue>。而如果是一些线程只添加元素,一些线程只移除元素,最好使用生产者/消费者集合
(6)函数式OOP
- 异步编程是函数式的(functional),.NET 引入的async让开发者进行异步编程的时候也能用过程式编程的思维来进行思考,但是在内部实现上,异步编程仍然是函数式的
伟人说过,世界既是过程式的,也是函数式的,但是终究是函数式的
可以用await等待的是一个类(如Task对象),而不是一个方法。可以用await等待某个方法返回的Task,无论它是不是async方法。
类的构造函数里是不能进行异步操作的,一般可以使用如下方法。相应的,我们可以通过
var instance=new Program.CreateAsync();
class Program
{
private Program()
{
}
private async Task<Program> InitializeAsync()
{
await Task.Delay(TimeSpan.FromSeconds(1));
return this;
}
public static Task<Program> CreateAsync()
{
var result = new Program();
return result.InitializeAsync();
}
}
- 在编写异步事件处理器时,事件参数类最好是线程安全的。要做到这点,最简单的办法就 是让它成为不可变的(即把所有的属性都设为只读)
(7)同步
同步的类型主要有两种:通信和数据保护
如果下面三个条件都满足,就需要用同步来保护共享的数据
- 多段代码正在并发运行
- 这几段代码在访问(读或写)同一个数据
- 至少有一段代码在修改(写)数据
- 观察以下代码,确定其同步和运行状态
class SharedData
{
public int Value { get; set; }
}
async Task ModifyValueAsync(SharedData data)
{
await Task.Delay(TimeSpan.FromSeconds(1));
data.Value = data.Value + 1;
}
// 警告:可能需要同步,见下面的讨论。
async Task<int> ModifyValueConcurrentlyAsync()
{
var data = new SharedData();
// 启动三个并发的修改过程。
var task1 = ModifyValueAsync(data);
var task2 = ModifyValueAsync(data);
var task3 = ModifyValueAsync(data);
await Task.WhenAll(task1, task2, task3);
return data.Value;
}
本例中,启动了三个并发运行的修改过程。需要同步吗?答案是“看情况”。如果能确定 这个方法是在 GUI 或 ASP.NET 上下文中调用的(或同一时间内只允许一段代码运行的任 何其他上下文),那就不需要同步,因为这三个修改数据过程的运行时间是互不相同的。 例如,如果它在 GUI 上下文中运行,就只有一个 UI 线程可以运行这些数据修改过程,因 此一段时间内只能运行一个过程。因此,如果能够确定是“同一时间只运行一段代码”的 上下文,那就不需要同步。但是如果从线程池线程(如 Task.Run)调用这个方法,就需要同步了。在那种情况下,这三个数据修改过程会在独立的线程池线程中运行,并且同时修改 data.Value,因此必须同步地访问 data.Value。
不可变类型本身就是线程安全的,修改一个不可变集合是不可能的,即便使用多个Task.Run向集合中添加数据,也并不需要同步操作
线程安全集合(例如 ConcurrentDictionary)就完全不同了。与不可变集合不同,线程安 全集合是可以修改的。线程安全集合本身就包含了所有的同步功能
关于锁的使用,有四条重要的准则
- 限制锁的作用范围(例如把lock语句使用的对象设为私有成员)
- 文档中写清锁的作用内容
- 锁范围内的代码尽量少(锁定时不要进行阻塞操作)
- 在控制锁的时候绝不运行随意的代码(不要在语句中调用事件处理,调用虚拟方法,调用委托)
如果需要异步锁,请尝试 SemaphoreSlim
不要在 ASP. NET 中使用 Task.Run,这是因为在 ASP.NET 中,处理请求的代码本来就是在线程池线程中运行的,强行把它放到另一个线程池线程通常会适得其反
(7) 实用技巧
- 程序的多个部分共享了一个资源,现在要在第一次访问该资源时对它初始化
static int _simpleValue;
static readonly Lazy<Task<int>> MySharedAsyncInteger =
new Lazy<Task<int>>(() =>
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(2));
return _simpleValue++;
}));
async Task GetSharedIntegerAsync()
{
int sharedValue = await MySharedAsyncInteger.Value;
}
《C#并发编程经典实例》笔记的更多相关文章
- HTML+CSS笔记 CSS笔记集合
HTML+CSS笔记 表格,超链接,图片,表单 涉及内容:表格,超链接,图片,表单 HTML+CSS笔记 CSS入门 涉及内容:简介,优势,语法说明,代码注释,CSS样式位置,不同样式优先级,选择器, ...
- CSS笔记--选择器
CSS笔记--选择器 mate的使用 <meta charset="UTF-8"> <title>Document</title> <me ...
- HTML+CSS笔记 CSS中级 一些小技巧
水平居中 行内元素的水平居中 </a></li> <li><a href="#">2</a></li> &l ...
- HTML+CSS笔记 CSS中级 颜色&长度值
颜色值 在网页中的颜色设置是非常重要,有字体颜色(color).背景颜色(background-color).边框颜色(border)等,设置颜色的方法也有很多种: 1.英文命令颜色 语法: p{co ...
- HTML+CSS笔记 CSS中级 缩写入门
盒子模型代码简写 回忆盒模型时外边距(margin).内边距(padding)和边框(border)设置上下左右四个方向的边距是按照顺时针方向设置的:上右下左. 语法: margin:10px 15p ...
- HTML+CSS笔记 CSS进阶再续
CSS的布局模型 清楚了CSS 盒模型的基本概念. 盒模型类型, 我们就可以深入探讨网页布局的基本模型了.布局模型与盒模型一样都是 CSS 最基本. 最核心的概念. 但布局模型是建立在盒模型基础之上, ...
- HTML+CSS笔记 CSS进阶续集
元素分类 在CSS中,html中的标签元素大体被分为三种不同的类型:块状元素.内联元素(又叫行内元素)和内联块状元素. 常用的块状元素有: <div>.<p>.<h1&g ...
- HTML+CSS笔记 CSS进阶
文字排版 字体 我们可以使用css样式为网页中的文字设置字体.字号.颜色等样式属性. 语法: body{font-family:"宋体";} 这里注意不要设置不常用的字体,因为如果 ...
- HTML+CSS笔记 CSS入门续集
继承 CSS的某些样式是具有继承性的,那么什么是继承呢?继承是一种规则,它允许样式不仅应用于某个特定html标签元素,而且应用于其后代(标签). 语法: p{color:red;} <p> ...
- HTML+CSS笔记 CSS入门
简介: </span>年的圣诞节期间,吉多·范罗苏姆为了在阿姆斯特丹打发时间,决心开发一个新的<span>脚本解释程序</span>,作为ABC语言的一种继承. & ...
随机推荐
- C#3.0扩展方法学习篇
什么是类的扩展方法 扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型.重新编译或以其他方式修改原始类型. MSDN Extension methods enable you to &q ...
- Jquery源码学习(第一天)
jQuery是面向对象的设计通过window.$ = window.jQuery = $; 向外提供接口,将$挂在window下,外部就可以使用$和jQuery $("#div1" ...
- SQLSERVER中的ALL、PERCENT、CUBE关键字、ROLLUP关键字和GROUPING函数
SQLSERVER中的ALL.PERCENT.CUBE关键字.ROLLUP关键字和GROUPING函数 先来创建一个测试表 USE [tempdb] GO )) GO INSERT INTO [#te ...
- 【CSS3进阶】酷炫的3D旋转透视
之前学习 react+webpack ,偶然路过 webpack 官网 ,看到顶部的 LOGO ,就很感兴趣. 最近觉得自己 CSS3 过于薄弱,想着深入学习一番,遂以这个 LOGO 为切入口,好好研 ...
- ASP.Net请求处理机制初步探索之旅 - Part 4 WebForm页面生命周期
开篇:上一篇我们了解了所谓的请求处理管道,在众多的事件中微软开放了19个重要的事件给我们,我们可以注入一些自定义的业务逻辑实现应用的个性化设计.本篇,我们来看看WebForm模式下的页面生命周期. ( ...
- 一个App完成入门篇-终结篇(八)- 应用收官
经过以上几步的学习,我们终于来到最后一个步骤了,应用APP也接近尾声. 通过之前的几节教程,不知道您对使用DeviceOne开发一个应用是不是已经得心应手了,本节教程将教会大家如何在开发完成之后通过D ...
- 扩展Bootstrap Tooltip插件使其可交互
最近在公司某项目开发中遇见一特殊需求,请笔者帮助,因此有了本文的插件.在前端开发中tooltip是一个极其常用的插件,它能更好向使用者展示更多的文档等帮助信息.它们通常都是一些静态文本信息.但同事他们 ...
- .NET实现微博粉丝服务平台接口
[文章摘要]Senparc.Weixin.MP虽然是微信公众号的SDK,但由于易信公众号和新浪微博粉丝服务平台也提供了微信兼容接口,所以也可以使用其快速实现相应的服务,当然微博由于与微信存在差异,如果 ...
- 模糊测试(fuzz testing)介绍(一)
模糊测试(fuzz testing)是一类安全性测试的方法.说起安全性测试,大部分人头脑中浮现出的可能是一个标准的“黑客”场景:某个不修边幅.脸色苍白的年轻人,坐在黑暗的房间中,正在熟练地使用各种工具 ...
- Hystrix框架3--线程池
线程池 在Hystrix中Command默认是运行在一个单独的线程池中的,线程池的名称是根据设定的ThreadPoolKey定义的,如果没有设置那么会使用CommandGroupKey作为线程池. 这 ...