Open DavidZhiXing opened 3 years ago
淘宝技术这十年 子柳
◆ 第0章 引言:光棍节的狂欢
师正紧盯着网站的流量和交易数据。
Amazon的财报
eBay的2011年财报
仅用于生成www.taobao.com首页的服务器就可能有成百上千台
其中最关键的便是LVS(Linux Virtual Server,
你的浏览器在同一个域名下并发加载的资源数量是有限的
我访问淘宝网首页需要加载126个资源,那么如此小的并发连接数自然会加载很久
分布在多个域名下,变相地绕过浏览器的这个限制
利用一些手段保证你访问的(这里主要指JS、CSS、图片等)站点是离你最近的CDN节点
淘宝开发了分布式文件系统TFS(TaoBao File System)来处理这类问题
基于一个分词库进行分词操作
把中文的汉字序列切分成有意义的词,就是中文分词
进行购物意图分析
浏览型
查询型
对比型
确定型
一千多台搜索服务器完成的
你仍然能够通过“已买到的宝贝”查看当时的快照。
商品详情快照进行保存和快速调用不是一件简单的事情
其中较为重要的是Tair(淘宝自行研发的分布式KV存储方案)。
你的这些访问行为都会如实地被系统记录下来,
淘宝研发了TimeTunnel,用于进行实时的数据传输,然后交给后端系统进行计算报表等操作
有些数据使用了压缩比高达1∶120的极限存储技术
云梯的基于Hadoop的由3000多台服务器组成的超大规模数据系统,以及一个基于阿里巴巴集团自主研发的ODPS系统的数据系统
能够知道小到你是谁,你喜欢什么,你的孩子几岁了,你是否在谈恋爱,喜欢玩魔兽世界的人喜欢什么样的饮料等
大到各行各业的零售情况、各类商品的兴衰消亡等海量的信息
带给各家银行和快递公司的流量也让他们如临大敌
◆ 第1章 个人网站
买一个什么样的网站
无须编译,发布快速,PHP语言功能强大,能做从页面渲染到数据访问所有的事情,而且用到的技术都是开源、免费的
买来之后不是直接就能用的,需要很多本地化的修改
把交易系统和论坛系统的用户信息打通
给运营人员开发出后台管理的功能(Admin系统
把交易类型
拍卖、一口价、求购商品、海报商品
一台负责发送Email、一台负责运行数据库、一台负责运行WebApp。
写数据的时候会把表锁住。
◆ LAMP架构的网站
他们看了PayPal的支付方式,发现不能解决问题
突然想到了“担保交易”这种第三方托管资金的办法
开发与银行网关对接功能
每天手工核对账单
一大堆银行卡摆在桌子上
第一招是免费
那么第二招就是“安全支付”
而淘宝的第三招就是“旺旺
收购了Skype之后也没有用到电子商务中去
最早做旺旺开发的人只有一个——无崖子,
为了支持他的工作,我们工作用的IM工具仅限于旺旺
开发出了一套旺旺表情
聊天室
◆ 武侠和倒立文化的起源
如果你在这个创业团队中,请什么样的人来做这件事
其中有一个网站就是eBay,那时eBay的系统刚刚从C++改到Java,而且就是请Sun的工程师给改造成Java架构的
利用Python进行数据分析(原书第2版) 韦斯·麦金尼
◆ 第1章 准备工作
表格型的数据
多维数组(矩阵)
由键位列关联的多张表数据
均匀或非均匀的时间序列
◆ 1.2 为何利用Python进行数据分析
比如Rails(Ruby)和Django(Python)进行网站搭建
因为它们可以用于快速编写小型程序、脚本或对其他任务进行自动化
很容易整合C、C++和FORTRAN等语言的代码
很多公司和国家实验室都使用Python将过去数十年产生的存量软件黏合在一起
比如用SAS或R针对想法进行研究、原型实现和测试
再将这些想法迁移到用Java、C#或C++编写的大型生产环境上
研究和原型实现
低延迟、高资源利用要求的应用时(例如高频交易系统
当搭建高并发、多线程应用
原因在于Python拥有全局解释器锁(GIL),这是一种防止解释器同时执行多个Python指令的机制
◆ 1.3 重要的Python库
更具交互性的Python解释器
它使用了一种执行-探索工作流来替代其他语言中典型的编辑-编译-运行工作流
针对操作系统命令行和文件系统的易用接口
◆ 第10章 数据聚合与分组操作
对数据集进行分类,并在每一组上应用一个聚合函数或转换函数,这通常是数据分析工作流中的一个重要部分
◆ 13.1 pandas与建模代码的结合
使用pandas用于数据载入和数据清洗,之后切换到模型库去建立模型是一个常见的模型开发工作流
程序员修炼之道:从小工到专家 Andrew Hunt David Thomas
◆ 36 需求之坑
需求、政策和实现之间的区别可能会变得非常模糊
找出用户为何要做特定事情的原因、而不只是他们目前做这件事情的方式,这很重要
有一种能深入了解用户需求、却未得到足够利用的技术:成为用户
在交易所里工作一周
Work with a User to Think Like a User
◆ 第8章 注重实效的项目 Pragmatic Projects
你就需要建立一些基本原则,并相应地分派任务
使你的各种工作流程自动化
在“极大的期望”中,我们将向你说明一些能使每个项目的出资人高兴的诀窍
◆ 41 注重实效的团队
帮助个体成为更好的程序员
团队必须为产品的质量负责
确保每个人都主动地监视环境的变化
让这个人持续地检查范围的扩大、时间标度的缩减、新增特性、新环境
团队本身也存在于更大的组织中
他们制作的文档新鲜、准确、一致
创立品牌
在与别人交谈时,大方地使用你的团队的名字。
确保一致和准确的一种很好的方式是使团队所做的每件事情自动化
担任工具构建员,构造和部署使项目中的苦差事自动化的工具。让它们制作makefile、shell脚本、编辑器模板、实用程序
◆ 42 无处不在的自动化
使用cron,我们可以自动安排备份、夜间构建、网站维护、以及其他任何可以无人照管地完成的事情
IDE有自身的优势,但只用IDE,可能很难获得我们寻求的自动化程度。我们想要用一条命令就完成签出、构建、测试和发布
有些项目具有各种必须遵循的管理工作流
◆ 附录A 资源Resources
在“专业协会”中,我们将详细介绍IEEE和ACM。我们建议注重实效的程序员加入其中一个
◆ 专业协会
讨论会和本地的会议能让你有大量机会接触兴趣相投的人
从高级的行业实践讨论直到低级的计算理论。
◆ 建设藏书库
下面是一些我们一周至少查看一次的链接。
C# 7.0核心技术指南(原书第7版) 约瑟夫·阿坝哈瑞 本·阿坝哈瑞
◆ 6.7 全球化
1.保证程序在其他文化环境中运行时不会出错。2.采用一种本地文化的格式化规则,例如日期的显示。3.设计程序,使之能够从将来编写和部署的附属程序集中读取文化相关的数据和字符串。
本地化则是为上面的最后一个任务针对特定文化编写附属程序集。
◆ 7.1 枚举
集合本身并不实现枚举器,而是通过IEnumerable接口提供枚举器:
通过定义一个返回枚举器的方法,IEnumerable灵活地将迭代逻辑转移到了另一个类上
Enumerable可以看作是“IEnumerator的提供者”
IEnumerator和IEnumerable总是和它们的扩展泛型版本同时实现:
避免了值类型元素装箱的额外开销,
数组就是一个很好的例子,它必须返回非泛型的(更确切的说是“经典”的)IEnumerator以避免破坏之前的代码。为了获得一个泛型IEnumerator
必须强制转换为相应的接口: 而且可以在枚举结束后(或中途停止后)确保释放这些资源。
因为它们能够在各种集合中实现所有元素类型的统一
为了支持foreach语句
为了与任何标准集合进行互操作· 为了达到一个成熟的集合接口的要求· 为了支持集合初始化器
· 如果这个类“包装”了任何一个集合,那么就返回所包装集合的枚举器· 使用yield return来进行迭代· 实例化自己的IEnumerator/IEnumerator
实现 除上述方式之外还可以创建一个现有集合的子类,
这种方法仅仅适合一些最简单的情况,那就是内部集合的元素正好就是所需要的那些元素
而更好的方法是使用C#的yield return语句编写迭代器
:GetEnumerator看起来并没有返回一个枚举器
但是,如果我们只需要一个简单的IEnumerable
实现,那么比起定义一个类,只用yield return语句就可以简洁地做到了 IEnumerable
.Current不需要将int转换为object从而避免了装箱开销。
◆ 7.2 ICollection和IList接口
,但是它们并没有提供确定集合大小、根据索引访问成员、搜索以及修改集合的机制
非泛型版本的存在只是为了兼容遗留代码。
提供一般的功能(例如Count属性)
· IList
/IDictionary<K, V> 及其非泛型版本:支持最多的功能(包括根据索引/键进行“随机”访问) 当需要编写一个集合类时,往往会从Collection
(请参见7.6节)派生 因此,ICollection
并没有实现ICollection;而IList 也没有实现IList IDictionary<TKey, TValue>也没有实现IDictionary
会得到一个同时含有Add(T)和Add(object)成员的接口。而这显著破坏了静态类型安全性,因为我们可以将任意类型作为Add方法的参数
ICollection
标准集合接口可以对其中的对象进行计数。它可以确定集合大小(Count),确定集合中是否存在某个元素(Contains),将集合复制到一个数组(ToArray)以及确定集合是否为只读(IsReadOnly) (Add)、删除(Remove)以及清空(Clear)操作
非泛型的ICollection也提供了计数的功能,但是它并不支持修改或检查集合元素的功能
而泛型版本则没有这些属性,因为线程安全性并非一个集合的固有特性
如果需要实现一个只读的ICollection
,则其Add、Remove和Clear方法应当直接抛出NotSupportedException。 它还可以按位置(通过索引器)读写元素,并在特定位置插入/删除元素。
通用的List
类实现了IList 和IList两种接口。 如果试图通过IList的索引器访问一个多维数组,程序就会抛出一个ArgumentException
由于上述类型参数仅仅在输出时使用,因此被标记为协变参数
但这并不意味着其底层实现也是只读的。
因为这意味着需要将成员从IList
移动到IReadOnyList 。而这会给CLR 4.5造成破坏性变化 IReadOnlyList
与Windows Runtime的IVetorView 相对应
◆ 7.3 Array类
但是IList
是显式实现的,以保证Array的公开接口中不包含其中的一些方法 静态的Resize方法。但是它实际上是创建一个新数组,并将每一个元素复制到新数组中
而且程序中其他地方的数组引用仍然指向原始版本的数组
rray本身是一个类。因此无论数组中的元素是什么类型,数组(本身)总是引用类型
结果是两个变量引用同一数组
tructuralComparisons类型访问它:
但是,其结果是一个浅表副本(shallow clone
即表示数组本身的内存会被复制
如果要进行深度复制即复制引用类型子对象,必须遍历整个数组,然后手动克隆每一个元素
(即令数组在理论上支持多至264个元素
因为CLR不允许任何对象(包括数组)在大小上超过2GB(不论是32位还是64位运行环境都如此)。
Array类的很多方法看起来应该是实例方法,实际上却是静态方法
非零开始的数组不符合CLS的规定(Common Language Specification,公共语言规范)
?原因就是object[]既不兼容多维数组
,也不兼容值类型数组以及不以零开始索引的数组
int[]数组不能够转换为object[],因此,我们需要Array类实现彻底的类型统一。
对于已知维数但未知类型的数组,泛型提供了一种更加简单且高效的方法:
。对于引用类型元素的数组,这意味着写入null值。而对于值类型元素的数组,这意味着调用值类型的默认构造函数
后者一般会将集合元素全部删除。
而Length和LongLength返回数组所有维度的元素总数。
GetLowerBound和GetUpperBound在处理非零起始的数组时是很有用的
一维数组元素的方法:
BinarySearch方法
· IndexOf / LastIndexOf方法:
Find/FindLast/FindIndex/FindAll/Exists/TrueForAll
二分搜索方法速度快,但是仅适用于排序数组
而且要求元素能够比较顺序,而不仅仅是比较是否相等
ICompare或者IComparable对象来判断元素的顺序
使用Lambda表达式可以将代码进一步简化为:
唯一的区别是FindAll返回满足条件的元素数组,而后者则返回一个IEnumerable
。 实现一个IComparer/IComparer
对象(请参见本章的7.7节)· 传递一个Comparison委托: rderBy与ThenBy运算符来作为Sort的替代方法
。和Array.Sort不同,LINQ运算符不会对原始数组进行修改,而是将排序结果放在新生成的IEnumerable
序列中 Clone、CopyTo、Copy和ConstrainedCopy。前两个是实例方法;后两个为静态方法
CopyTo和Copy方法复制数组中的若干连续元素
若复制多维数组则需要将多维数组的索引映射为线性索引
ConstrainedCopy执行一个原子操作:如果所有请求的元素无法成功复制(例如类型错误)那么操作将会回
滚
Array还提供了一个AsReadOnly方法来包装数组以防止其中的元素被重新赋值。
它会调用Converter委托来复制每一个元素
。此后,也可以调用Enumerable类的ToArray方法将其转换回数组形式
◆ 7.4 List、Queue、Stack和Set
本节介绍的集合类型中,泛型List类是最常用的
在集合中追加元素的效率很高(因为数组末尾一般都有空闲的位置),而插入元素的速度会慢一些(因为插入位置之后的所有元素都必须向后移动才能留出插入空间)
但其他情况下就需要检查每一个元素,因而效率就不是那么高了。
当需要一个不共享任何相同基类(object除外)的混合类型元素列表时
在这种情况下,如果需要使用反射机制(见第19章)处理列表,那么选择使用ArrayList更有优势
反射机制更容易处理非泛型的ArrayList。
LinkedList
是一个泛型的双向链表(见图7-4) 它的主要优点是元素总能够高效插入到链表的任意位置
因为插入节点只需要创建一个新节点,然后修改引用值
因为链表本身并没有直接索引的内在机制。我们必须遍历每一个节点,并且无法执行二分搜索
但是没有实现IList
,因为它不支持索引访问 它们还包括一个只返回而不删除队列第一个元素的Peek方法
以及一个Count属性(可在取出元素前检查该元素是否存在于队列中)。
队列具有一个直接指向头部和尾部元素的索引
并也提供了一个只读取而不删除元素的Peek方法、Count属性,以及可以导出数据并进行随机访问的ToArray方法:
BitArray是一个压缩保存bool值的可动态调整大小的集合
BitArray具有更高的内存使用效率。
它提供了四种按位操作的运算符方法:And、Or、Xor和Not
· 它们的Contains方法均使用散列查找因而执行速度很快
它们都不保存重复元素,并且都忽略添加重复值的请求。
无法根据位置访问元素。
这些类型的共同点是它们都实现了ISet
接口。 System.Core.dll中,而SortedSet
和ISet 位于System.dll中。 HashSet
是通过使用只存储键的散列表实现的 一个红/黑树实现的
SortedSet
拥有HashSet 的所有成员
◆ 7.5 字典
元素是否按照有序序列存储· 元素是否可以以位置(索引)或键进行访问· 是否为泛型· 从大字典中用键获得元素值时的快慢
它与Windows Runtime类型的IMapView<K, V>接口相对应,而只读接口也是出于这个原因才引入的。
我们可以使用索引器或者TryGetValue方法从字典中检索元素的值
或者我们还可以用属性ContainsKey来确定键是否存在,但这样做意味着需要进行两次查找才能最终检索值
因为一些遗留代码(包括.NET Framework本身)仍然在使用IDictionary。
试图通过索引器检索一个不存在的键会返回null(而不是抛出一个异常)。
· 使用Contains而非ContainsKey来检测成员是否存在。
枚举非泛型的IDictionary会返回一个DictionaryEntry结构体序列:
Dictionary同时实现了泛型和非泛型的IDictionary接口。
字典的底层散列表会将每一个元素的键转换为一个整数散列码,
然后使用算法将散列码转换为一个散列键
如果这个“桶”包含了不止一个值,那么散列表会在其中执行线性搜索
而是尽可能地令散列码均匀分布在32位整数范围内
避免出现元素过度集中(低效)的桶
,只要可以对键进行相等比较并获得散列码即可。
如果要改变这种行为,可以重写这些方法,或在创建字典的时候提供一个IEqualityComparer对象
指定一个不区分大小写的相等比较器:
而不是根据其内部的实现方式命名(Hashtable、ArrayList)
但是这也意味着它们的名称无法反映其性能表现(而通常这是选择集合的重要依据)。
它能够保存添加元素时的原始顺序
使用OrderedDictionary既可以根据索引访问元素,也可以根据键来访问元素。
OrderedDictionary是Hashtable和ArrayList的组合
ListDictionary使用一个独立链表来存储实际的数据
ListDictionary在处理大型列表时非常缓慢
它存在的真正意义是高效处理非常小的列表(小于10个元素)
HybridDictionary是一个在达到一定大小后能够自动转换为Hashtable的ListDictionary
它的原理在于在字典很小时降低内存开销,而在字典变大时保持良好性能
因此即使直接使用Dictionary也是非常合理的。
SortedDictionary<, >内部为红黑树:一种在插入和检索中表现都相当不错的数据结构
SortedList<, >内部实现为排序的数组对
它的检索速度很快(通过二分搜索)但插入性能很差(
直接访问排序列表中的第n个元素
而在SortedDictionary<, >中只能够通过枚举n个元素才能实现相同的操作
与所有字典一样,以上三种集合都不允许出现重复键。
可以将相同键的多个值保存在一个列表中:
◆ 7.6 自定义集合与代理
但它们无法令你控制当一个元素添加到集合或从集合移除时的行为
当添加或删除一个元素时触发一个事件。· 当添加或删除一个元素时更新一些属性。· 检测“不合法”的添加/删除操作并抛出异常(例如,如果操作违反了业务规则)
这些类型都是一些实现了IList
或者IDictionary<, >的代理类或包装类,它们将方法转发到一个底层集合上。 Collection
类是一个可定制的List 包装类 这些虚方法提供了类似“钩子”的入口,你可以通过它们强化或更改列表的正常行为
它的作用是提供一个基类以便扩展
以便自动维护这个属性:
和其他集合类不同的是,它所提供的列表是代理而非复制的
CollectionBase是Collection
的非泛型版本 CollectionBase没有模板方法InsertItem、RemoveItem
SetItem和ClearItem,但它对应每一个函数都设置了相应的“钩子”方法来满足各种需要
所以在继承它时还必须实现类型化方法,至少需要一个类型化的索引器和Add方法
它增加了通过键访问元素的功能(和字典类似),移除了代理操作自己内部列表的能力。
这意味着枚举一个KeyedCollection就像是在枚举一个普通列表一样
KeyedCollection的非泛型版本称为DictionaryBase。这个遗留类采用了非常特别的实现方法:它实现了IDictionary,并且使用了类似CollectionBase那样的烦琐的钩子函数
◆ 12.1 IDisposable接口、Dispose方法和Close方法
第三种情况包含以下的类型:WebClient、StringReader、StringWriter和(System.ComponentModel命名空间下的)BackgroundWorker
但是如果该对象需要长时间使用,那么持续追踪并在其不再使用的情况下进行销毁就会带来不必要的复杂性。
◆ 14.1 概述
如果使用ASP.NET、WCF或者Web Services,则.NET Framework会自动执行并行处理
在多核主机上,有时可通过预测的方式提前执行某些任务来改善程序性能
◆ 14.2 线程
它们共享同一个执行环境(特别是内存
可以使用一个线程在后台获得数据,同时使用另一个线程显示所获得的数据
任务是一个中间层,它增加了学习的复杂性
(控制台、WPF、UWP或者Windows Forms)在启动时都会从操作系统自动创建一个线程(主线程)
要创建并启动一个线程,需要首先实例化Thread对象并调用Start方法
每一个线程划分时间片(Windows系统的典型值为20毫秒
而在一个多核心的机器上,两个线程可以并行执行(会和机器上其他执行的进程进行竞争
但这却是由于Console处理并发请求的机制导致的。
当Thread的构造函数接收的委托执行完毕之后,线程就会停止。线程停止之后就无法再启动了。
因为线程的名称会显示在“Thread”窗口和“Debug Location”工具栏上
调用Thread的Join方法可以等待线程结束:
Thread.Sleep(0)将会导致线程立即放弃自己的时间片
Thread.Yield()执行相同的操作,但是它仅仅会将资源交给同一个处理器上运行的线程
Sleep(0)或者Yield在高级性能调优方面非常有用
如果在代码的任意位置插入Thread.Yield()导致程序失败,则代码一定存在缺陷
。阻塞的线程会立刻交出它的处理器时间片,并从此开始不再消耗处理器时间
可以使用ThreadState属性测试线程的阻塞状态
以下的扩展方法将ThreadState限定为以下四个有用的值之一:Unstarted、Running、WaitSleepJoin、Stopped:
ThreadState属性适用于诊断调试工作,但是不适合实现同步
如果一个操作的绝大部分时间都在等待事件的发生,则称为I/O密集
如果操作的大部分时间都用于执行大量的CPU操作,则称为计算密集。
要么在当前线程同步进行等待,直至操作完成(例如Console.ReadLine、Thread.Sleep以及Thread.Join);要么异步进行操作,在操作完成的时候或者之后某个时刻触发回调函数(之后将详细介绍
但是也可能在一个定期循环中自旋
但另外一种选择是令线程持续性自旋:
因此从效果上来说我们将一个I/O密集的操作转变成了一个计算密集型操作。
其次,阻塞并非零开销
每一个线程在存活时会占用1MB的内存
因此,这些程序更适于使用回调的方式,在等待时完全解除这些线程。
如果不同的线程拥有同一个对象的引用,则这些线程之间就共享了数据
编译器会将Lambda表达式捕获的局部变量或匿名委托转换为字段,因此它们也可以被共享
而静态字段提供了另一种在线程之间共享变量的方法:
当一个线程在判断if语句的时候,另一个线程有可能在done设置为true之前就已经开始执行WriteLine语句了。
然而,最好的方式是避免使用共享状态
底层处理器也会采用独立的读-自增-写操作来执行x++这
锁并非解决线程安全的银弹,人们很容易忘记在访问字段时加锁,而且锁本身也存在一些问题(例如死锁)
访问那些存储频繁访问数据库对象的共享缓存
如之前所见,Lambda表达式是向线程传递参数的最方便的形式之一。
C#捕获变量的规则不论在循环还是在多线程中都容易造成各种各样的问题。
线程执行和线程创建时所处的try/catch/finally语句块无关
解决方法是将异常处理器移动到Go方法之内:
应用程序的所有线程入口方法都需要添加一个异常处理器,就和主线程中一样(通常位于更高一级的执行栈中)。未处理的异常可能会导致整个应用程序崩溃,并弹出丑陋的错误对话框。
通常需要明确记录异常的信息
最后还可以选择是否重新启动程序,因为未处理的异常可能会使应用程序处于无效状态。
这种方式非常适合于记录日志并报告应用程序的缺陷
它不会被非UI线程中发生的未处理异常触发)
但是为避免应用程序在出现未处理异常后继续执行造成潜在的状态损坏,因此通常需要重新启动应用程序。
AppDomain.CurrentDomain.UnhandledException事件会在任何线程出现未处理异常时触发
如果在非默认应用程序域中出现未处理的异常,则可以销毁并重新创建应用程序域而无须重启整个应用程序
可以使用线程的IsBackground属性来查询或修改线程的前后台状态:
主线程结束时,由于前台线程仍然在运行,因此应用程序会继续保持运行状态。
如果应用程序在finally或者using块中执行了清理逻辑,例如删除临时文件,那么可以在应用程序结束时显式等待后台线程汇合(jon)
都需要指定一个超时时间,来抛弃那些无法按时结束的问题线程。否则用户只能够通过“任务管理器”来终止应用程序了。
活跃的前台线程是导致应用程序无法正常退出的常见原因之一。
这种方法非常适用于一些工作量比较少,但是要求较低延迟(能够快速响应)的UI进程中
ManualResetEvent是CLR提供的若干信号发送结构之一,
然而,所有富客户端应用程序采取的线程模型都要求UI元素和控件只能够由创建它们的线程访问
如果想要在工作线程上更新UI,就必须将请求发送给UI线程
· 在WPF中,调用元素上的Dispatcher对象的BeginInvoke或Invoke方法。
在UWP应用中,可以调用Dispatcher对象的RunAsync或Invoke方法。
用控件的BeginInvoke或Invoke方法。
所有这些方法都接收一个委托来引用实际执行的方法
会将这个委托加入到UI线程的消息队列上
这个消息队列也处理键盘、鼠标和定时器事件
而如果不需要返回值,则可以使用BeginInvoke/RunAsync,
它们不会阻塞调用者,也不会造成死锁
这种循环可以令工作线程将委托封送到UI线程上执行。
UI线程也可以有多个,但是每一个线程要对应不同的窗口
通常称之为单文档界面(Single Document Interface, SDI)应用程序
而和其他的SDI窗口在功能上完全独立。
通过捕获这个属性就可以从工作线程将数据“提交”到UI控件上:
因为它同样适用于所有富客户端用户界面的API(Synchro-nizationContext还有一个专门支持ASP.NET的子类
在Dispatcher或者Control上调用Post的效果和BeginInvoke相同。
Send方法和Invoke方法的效果也是相同的。
Back-groundWorker在Task和异步函数引入之后就弃用了,而它们同样使用了SynchronizationContext
都需要一定的时间(几百毫秒)来创建新的局部变量栈
而线程池通过预先创建一个可回收线程的池子来降低这个开销
它可以支持运行一些短暂的操作而不会受到线程启动开销的影响。
线程池中线程的Name属性是无法进行设置的,因此会增加代码调试的难度
线程池中的线程都是后台线程。
而当我们将线程归还线程池时其优先级会恢复为普通级别
Thread.CurrentThread.IsThreadPoolThread属性可用于确认当前运行的线程是否是一个线程池线程。
NET Framework 4.0之前没有Task类,因此可以调用ThreadPool.QueueUser-WorkItem:
WCF、Remoting、ASP.NET和ASMX Web Service应用服务器· System.Timers.Timer和System.Threading.Timer
BackgroundWorker类(已弃用)· 异步代理(已弃用)
导致操作系统必须按时间片执行线程调度
并可能使CPU的缓存失效
CLR通过将任务进行排队,并控制任务启动数量来避免线程池超负荷
然后通过爬山算法调整并发数量
哪怕计算机上有多个进程活动,它仍能够运行在最优性能曲线上。
大多数工作项目的运行时间非常短暂(小于250毫秒或者理想情况下小于100毫秒)
线程池中不会出现大量以阻塞为主的作业。
阻塞是非常麻烦的。因为它会让CLR错误地认为它占用了大量的CPU
特别是应用程序的生命周期前期
◆ 14.3 任务
但是当线程Join后却难以从中得到“返回值”
捕获和处理线程中操作抛出的异常也是非常麻烦的
在线程完成之后,就无法再次启动它,相反只能够将其Join
这种方式难以将小的并发组合成为大的并发操作
Task是一个更高级的抽象概念,它代表了一个并发操作
可以通过TaskCompletionSource采用回调的方式避免多个线程同时等待I/O密集型操作
(例如在任务对象上调用Wait,或者调用Console.ReadLine()方法)
这是因为LINQPad进程会保持后台线程的运行
Task.Run会返回一个Task对象,它可以用于监控任务的执行过程
调用Task的Wait方法可以阻塞当前方法,直到任务完成
如果要执行长时间阻塞的操作(如上面的例子)则可以按照以下方式避免使用线程池线程:
如果运行的是I/O密集型任务,则使用TaskCompletionSource和异步函数(asynchronous functions)通过回调函数而非使用线程实现并发性。
如果任务是计算密集型,则使用生产者/消费者队列可以控制这些任务造成的并发数量,避免出现线程和进程饥饿的问题
以下示例创建了一个任务,它使用LINQ计算前三百万个整数(从2开始)中素数的个数:
可以将Task
理解为一个“未来值”,它封装了Result并将在之后生效。 Wait()或者访问Task
的Result属性时,该异常就会被重新抛出 CLR会将异常包装为一个AggregateException
使用Task的IsFaulted和IsCanceled属性可以在不抛出异常的情
况下检测出错的任务
自治任务指那些可以“运行并忘记”的任务
防止出现和线程类似的难以察觉的错误。
自治任务中的未处理异常称为未观测异常(unobserved exception)。在CLR 4.0中
CLR会在终结线程上重新抛出该异常)
然而由于错误实际发生的时间和垃圾回收的时间间隔可能非常大,
如果异常仅导致无法获得一些不重要的结果,那么忽略异常是最好的方式
这个缺陷可能会是程序陷入无效的状态;· 这个缺陷可能导致更多异常的发生,这样,在没有记录初始异常的情况下会增加诊断的难度
使用静态事件TaskScheduler.UnobservedTaskException可以在全局范围订阅未观测的异常
如果在等待任务时设置了超时时间,则在超时时间之后发生的错误将产生未观测异常
在错误发生之后,如果检查任务的Exception属性,则该异常就成为了已观测到的异常。
延续通常由一个回调方法实现
调用任务的GetAwaiter方法将返回一个awaiter对象
当它执行完毕(或者出现错误)时调用一个委托
awaiter可以是任意暴露了OnCompleted和GetResult方法和IsCom-pleted属性的对象
(实际上OnCompleted是INotifyCompletion接口的一部分
对于非泛型任务,GetResult的返回值为void,而这个函数的用途完全是为了重新抛出异常。
如果提供了同步上下文,则OnCompleted就会自动捕获它,并将延续提交到这个上下文中
我们可以使用ConfigureAwait方法来避免这种行为:
延续代码一般会运行在先导任务运行的线程上,从而避免不必要的开销。
ContinueWith更适用于并行编程场
TaskCompletionSource可以创建一个任务
这非常适用于I/O密集型的工作
能够传递返回值、异常或延续
以上方法都应当只调用一次。如果多次调用的话SetResult、SetException或者SetCanceled会抛出异常
TaskCompletionSource的真正作用是创建一个不绑定线程的任务。
我们可以使用Timer类,由CLR(进而由操作系统)在x毫秒之后触发一个事件(我们将在22.11节介绍定时器),而无须使用线程:
但是我们并没有非泛型的Task-CompletionSource类,因此我们无法直接返回一个非泛型的Task。
TaskCompletionSource不需要使用线程,意味着只有当延续启动的时候(5秒钟之后)才会创建线程。
如果请求的速度超过了处理的速度,那么线程池就会进行排队
这种方法最适合处理执行时间短暂的线程密集的作业
Task.Delay是Thread.Sleep的异步版本
◆ 14.4 异步原则
并且异步调用需要并发创建
异步编程的原则是以异步的方式编写运行时间很长(或者可能很长)的函数。
异步方法的不同点在于并发性是在长时间运行的方法内启动的
I/O密集的并发性的实现不需要绑定线程
而在于线程的效率。特别是,每一个网络请求不要独自消耗一个线程
为了处理程序的复杂性,一般来说会将大的方法重构为若干个小的方法,从而产生一连串互相调用的方法(调用图(calling graph
因此,我们最终会得到一个跨越很多方法的并发操作(粗粒度并发性),此时需要考虑图中每一个方法的线程安全性
常用的经验法则是任何超过50毫秒的响应都用异步的方式处理。
以至于一些运行时间较长的方法要么没有同步执行的版本,要么会抛出异常。
任务非常适合进行异步编程,因为它支持延续
访问控件、共享状态而不用担心会出现线程安全问题
以上代码是如何工作的并不重要,重要的在于它会运行相当一段时间
采用以下的方法就可以为这个调用图创建粗粒度的并发性:
相反,如果采用细粒度的并发性,
我们就需要编写异步的GetPrimesCount方法:
这个循环将很快完成10次迭代(因为这些方法都是非阻塞的),然后全部的10个操作都会并行执行
在实际中,若任务B依赖任务A的结果,则我们有充分的理由来保证任务执行的顺序
幸运的是C#的异步函数(asynchronous function)可以帮我们完成所有这些操作。使用async和await关键字
就会发现循环结构(for、foreach等)和延续一起工作效果并不理想。这是因为循环依赖于方法当前的本地状态(循环还将执行多少次)
但是有时还是需要将循环结构替换为函数式的等价操作(即LINQ查询)。这也是Reactive Framework(Rx)的基础
它非常适合于在结果上执行查询运算符,或者合并多个序列。
◆ 14.5 C#的异步函数
async修饰符只支持返回类型为void以及(我们稍后会介绍的)Task或Task
的方法(或Lambda表达式)。 它只影响方法内部的执行细节
因此在接口上添加async是没有
意义的
当遇到await表达式时,通常情况下执行过程会返回到调用者上。就像是迭代器中的yield return一样。
返回之前会在等待的任务上附加一个延续
await表达式的最大优势在于它几乎可以出现在代码的任意位置
但不能出现在lock表达式、unsafe上下文,或者执行入口(Main方法)中。
由以上代码可见异步函数的简洁性:只需按同步方式书写,
为了利用这种模型的优点,真正并发的代码应避免访问共享状态或UI组件。
它的DownloadDataTaskAsync方法可以异步地将一个URI的内容下载到一个字节数组中,并返回Task<byte[]>对象
UI元素上绑定的事件处理器就是通过消息循环来执行的
且.NET Framework仅通过EAP和APM等模式(请参见14.7节),而不是以返回Task的方式暴露异步编程特性,导致异步编程举步维艰。
在一个富客户端场景下,若执行点并没有在UI线程上,则它会返回UI线程。在其他的场景下,它会继续在延续所在的线程上运行
调用异步方法但不等待就可以令异步方法和后续代码并行执行。
在这种情况下,我们可以定义一个共享字段_x,在GetAnswerToLife中不需要任何的锁保护就可以对其进行自增操作。
异步Lambda表达式可以附加到事件处理器:
◆ 14.6 异步模式
CLR拥有一对专门针对进度报告的类型:IProgress
接口和Progress 类(实现了IProgress 接口) IProgress
和异步函数返回的任务结合可以实现和IObserver 类似的功能 而IObserver
的OnNext提交的值则是最终的结果,而这也正是调用它的初衷。
◆ 15.1 .NET流的架构
后台存储、装饰器以及流适配器
后台存储是输入输出的终结点
· 支持顺序读取字节的源。· 支持顺序写入字节的目标
流会以每次一个字节或者每次一块数据的方式按照序列处理数据
例如FileStream或者NetworkStream
这些流会使用其他的流,并以某种方式转换数据。例如DeflateStream或者CryptoStream
后台存储流无须自己实现压缩和加密功能
装饰之后流不再受接口变化的影响。
装饰器支持运行时连接
装饰器可以相互串联
例如文本或者XML
TextReader有一个ReadLine方法
而XmlTextWriter则拥有WriteAttribute方法
但装饰器是一个流,而适配器本身不是一个流
而适配器则提供了处理更高级类型
◆ 15.2 使用流
读、写、查找
例如关闭、刷新(flush)和配置超时时间
它还提供了异步的Read和Write方法
并支持取消令牌。
有些应用程序可能需要操作速度相对缓慢的流(尤其是网络流)
Read方法可以将流中的一个数据块读到一个数组中
使用Read方法时,只有当方法返回0时才能够确定读到已经到达了
流的末尾
幸运的是,BinaryReader类型提供了实现相同效果的简单方法:
如果CanSeek返回true,那么表示当前的流是可以查找的
流在使用结束后必须销毁,以释放底层资源,例如文件和套接字句柄
关闭一个装饰器流会同时关闭装饰器及其后台存储流。
是会先将缓冲区填满再写入存储器。Flush方法可以强制将缓冲区中的数据写入后台存储中
在UWP应用中,应当使用Windows.Storage这个Windows Runtime类型来处理文件I/O操作
MemoryStream使用数组作为后台存储。这在一定程度上与使用流的目的是相违背的,因为这个后台存储都必须一次性地驻留在内存中
随机访问一个不可查找的流
而GetBuffer方法则更加高效地直接返回底层存储数组的引用
MemoryStream的关闭和刷新不是必须的
匿名管道(速度更快):支持在同一个计算机中的父进程和子进程之间进行单向通信。
命名管道(更加灵活):允许同一台计算机的任意两个进程之间,或者不同计算机(使用Windows网络)的两个进程间进行双向通信
它不依赖于任何网络传输(因此没有网络协议开销)
另一种进程通信的方法是通过共享内存进行通信
WCF和Remoting API都是提供了基于IPC通道的高级消息通信框架。
,因此双方不能同时发送或者接收消息
通信双方还需要统一每一次传输的数据长度
仅仅通过Read方法是否返回0来确定PipeStream是否完成了消息的读取是不行
与命名管道一样,客户端和服务器必须协调它们的发送和接收
种方法是在每一次传输的前四个字节中发送一个整数值,来定义后续消息的长度
BufferedStream可以装饰或者包装另外一个具有缓冲功能的流
在以下代码中,我们将一个File-Stream包装在一个有20KB缓冲区的BufferedStream中:
因此剩余的19999次ReadByte调用就不需要再次访问FileStream了。
这是因为FileStream中已经内置了缓冲区。它的唯一用途只是扩大一个已有的FileStream缓冲区而已
◆ 15.3 流适配器
文本适配器(处理字符串和字符数据
二进制适配器(处理基元类型数据,例如int、bool、string和float
XML适配器(见第11章)
它们在框架中各有两个通用的实现:
◆ 16.10 使用TCP
HTTP、FTP和SMTP使用TCP; DNS使用UDP
BitTorrent和Voice over IP都使用了UDP
但是相对于StreamReader和StreamWriter,其优势在于可以在编码中给字符串添加表示长度的整数前缀。
而如果调用StreamReader.ReadToEnd则可能会无限期阻塞,因为NetworkStream并没有结尾!
实际上StreamReader与NetworkStream是完全不同的
因为它只有在执行await表达式前后的代码时才会占用线程
◆ 16.11 使用TCP接收POP3邮件
辅助方法以无缓冲的方式读取一
我们还需要一个辅助方法来发送命令。
◆ 第18章 程序集
一个程序集包含单个的Windows可移植执行文件(Windows Portable Executable, PE)
◆ 18.1 程序集的组成部分
我们可以使用.NET的ildasm.exe查看程序集清单的内容
◆ 22.1 同步概述
同步(synchronization)是指协调并发操作,得到可以预测的结果的行为。
延续(continuation)和任务组合器
排它锁:排它锁每一次只允许一个线程执行特定的活动或一段代码。
非排它锁包括Semaphore(Slim)和ReaderWriterLock(Slim)。
信号发送结构:这种结构允许线程在接到一个或者多个其他线程的通知之前保持阻塞状态
一些结构在不使用锁的前提下也可以(巧妙的)处理特定的共享状态的同步操作,称为非阻塞同步结
它们包括Thread. MemoryBarrier、Thread.VolatileWrite、volatile关键字和Interlocked类
◆ 22.2 排它锁
· Mutex可以跨越多个进程(计算
· SpinLock可用于实现微优化
如果参与竞争的线程多于一个,则它们需要在准备队列中排队,并以先到先得的方式获得锁
本例中的锁保护了Go方法中的访问逻辑,也保护了_val1和_val2字段。
如果调用Monitor.Exit之前并没有对同一个对象调用Monitor.Enter,则该方法会抛出异常。
但若已经获得了锁,那么这个锁就永远无法释放,因为我们已经没有机会进入try/finally代码块了。因此这种情况会造成锁泄露
Enter方法执行结束后,当且仅当该方法执行时抛出了异常且没有获得锁时,lockTaken为false。
如果不给TryEnter方法提供任何参数,且当前无法获得锁,则该方法会立即超时。
若一个对象在各个参与线程中都是可见的,那么该对象就可以作为同步对象。
同步对象本身也可以是被保护的对象
如果一个字段仅作为锁存在(如前一节中的_locker),则可以精确地控制锁的范围和粒度
上述锁定方式有一个缺点,即无法封装锁逻辑,因此难以避免死锁或者长时间阻塞
而类型上的锁甚至可以跨越(同一进程中的)应用程序域的边界
锁本身不会限制同步对象的访问功能。
只有两个线程均执行lock(x)语句才会发生阻塞。
即便对于最简单的情况(例如对某个字段进行赋值),也必须考虑进行同步
为了提高性能,编译器、CLR乃至处理器都会调整指令的执行顺序并在CPU的寄存器中缓存变量值
使用锁可以避免第二个问题。因为锁会在其前后创建内存栅障
而指令执行顺序的重排和变量缓存是无法跨越这个围栏的。
若使用信号发送结构来确保同一时刻只有一个线程能对变量进行读写操作
也不可能被其他能够更改x和y的值,或是破坏其输出结果的线程抢占
在lock语句块内抛出异常可能会破坏通过锁实现的原子性
另一种更加复杂的解决方案是在catch或者finally块中实现“回滚”操作。
当一个锁中的方法调用另一个方法时,嵌套锁很奏效:
两个线程互相等待对方占用的资源就会使双方都无法继续执行
使用三个或者更多的线程则可能形成更加复杂的死锁链。
它不会自动检查和处理死锁(强制终止其中一个线程)。
除非指定超时时间,否则线程死锁将致使线程永久阻塞
它会自动检测死锁,然后在其中的一个线程抛出一个可捕获的异常。
死锁是多线程中最难解决的问题之一,尤其是当其涉及了很多相关对象时。而其中最难的部分是确定调用者持有了哪些锁。
同时,另外一个线程则按相反的顺序执行了锁定
面向对象的设计模式会加剧这个问题,因为这些设计模式会创建只有在运行时才能够确定的调用链。
因此最常见的建议是“使用一致的顺序锁定对象以避免死锁”。
当锁定一个对象的方法调用时,务必警惕该对象是否持有当前对象的引用
使用更高级的同步手段,例如任务的延续/组合器、数据并行、不可变类型(本章稍后会进行介绍)都可以减少对锁的依赖
在获得锁时调用其他代码就有可能造成封装锁的泄露。留意这种模式也有助于发现潜在的问题
在WPF应用程序中调用Dispatcher.Invoke或者在Windows Forms应用程序中调用Control.Invoke并同时占有锁时就可能出现死锁
通常,使用BeginInvoke(如果存在同步上下文,则还可以使用异步函数)替代Invoke就可以解决上述问题
当然,还可以在调用Invoke之前释放持有的锁,
Mutex和C#的lock类似,但是它可以支持多个进程
在非竞争的情况下获得或者释放Mutex需要大约一微秒的时间,大概比lock要慢20倍
跨进程Mutex的一个常见用途是保证一次只能够运行一个应用程序的实例。
如果想令其对所有终端服务可见,则需要在其名称中使用“Globa\”前缀。
◆ 22.3 锁和线程安全性
线程安全主要是通过锁以及减少线程间的交互性实现的。
· 使用线程安全的类型并不能保证程序是线程安全的
来保证顶层的序列访问性
而保证线程安全性是开发者的责任,一般都是通过排它锁实现的
线程间的交互也主要局限于静态字段。这些静态字段常用于在内存中缓存公共数据或提供基础服务,例如身份验证和审核服务。
即在UI线程上访问共享的状态
使用自动锁机制
只要调用该对象的方法或者属性,则在整个方法或属性运行期间会持有一个对象范围锁
。因此,在合理的自动锁机制出现之前,手动锁通常是更好的选择。
本例中,我们为_list对象本身添加了锁。如果有两个相互关联的列表,则必须使用一个公共的对象作为锁
NET中对集合进行枚举也不是线程安全的
这样就无须在非常耗时的枚举过程中持有排它锁了
有时,即使访问线程安全的对象也需要使用锁
不论列表是不是线程安全的,上述语句都绝不是线程安全的!
不仅如此,任何修改列表的代码都必须添加相同的锁才行
从而保证它不会在前面的语句中间执行。
(而这也使List是线程安全的这一假设变得毫无意义了)。
在高并发环境下为集合的访问
加锁可能会导致大量阻塞
将对象的访问包裹在自定义锁中这种方式只有在所有的线程都会访问并使用这个锁的情况下才有效
最坏的情况就是公有类型的公有静态成员了。
因此,DateTime的所有静态成员都小心地进行了实现以确保它们都是线程安全的
静态成员是线程安全的,而实例成员则不具备线程安全性
即,确保静态成员的线程安全性,为使用该类型的代码实现线程安全性提供了保障
静态方法的线程安全性是必须显式实现的。静态方法本身不会自动实现线程安全
如果一个类型对并发只读访问是线程安全的,那么不要在消费者认为是只读的操作中对字段进行任何的写操作
只读线程安全性也是将枚举器和可枚举对象分离的原因之一:
如果文档中并没有指出,则不能假定一个方法的只读性
其内部实现需要更新私有种子字段的值
一个典型的服务器类要么是无状态的(没有字段),要么其激活模型会为每一个客户端的每一个请求创建一个独立的对象实例
如果两个线程同时调用该方法,并且其id对应的数据并没有缓存,则RetrieveUser方法有可能会调用两次,而字典也会多更新一次
但如果这样的话,任何其他查询用户的请求就都会被阻塞,性能会更差
不可变对象的字段通常都声明为只读
不可变性是函数式编程的特点,它不修改对象,而是创建一个带有不同属性的新对象。LINQ就采用了这种编程范式。
除了在这些字段上加锁之外,我们还可以定义以下不可变类
现在,只需要在赋值语句上添加锁就可以读写该类型的值了:
◆ 22.4 非排它锁
一旦满员之后,就不允许其他人进入了,人们只能在外面排队
即俱乐部当前的空闲容量,以及俱乐部的总容量。
任何线程都可以调用Semaphore的Release方法
Mutex和lock则不然,只有持有锁的线程才能够释放锁。
它进行了一些优化以适应并行编程对低延迟的需求。
因为它可以在等待时指定一个取消令牌(请参见14.6.1)
但是它不能用于进程间通信
Semaphore在调用WaitOne和Release方法时大概会消耗1微秒的时间,而Sem-aphoreSlim的开销只有前者的十分之一
信号量可用于限制并发性,防止太多的线程同时执行特定的代码。
命名的Semaphore和Mutex一样是可以跨进程使用的。
通常,一个类型实例的并发读操作是线程安全的,而并发更新操作则不然
但是如果其读操作很多但更新操作很少,则使用单一的锁限制并发性就不太合理了
ReaderWriterLockSlim是专门为这种情形进行设计的,它可以最大限度地保证锁的可用性。
与常规的lock(Monitor.Enter/Exit)相比ReaderWriterLockSlim的执行速度仍然慢一倍
· 写锁是全局排它锁· 读锁可以兼容其他的读锁
一个持有写锁的线程将阻塞其他任何试图获取读锁或写锁的线程
但是如果没有任何线程持有写锁的话,那么其他任意数量的线程都可以并发获得读锁
三个线程将持续枚举列表中的元素,而另外两个线程则会每隔100毫秒生成一个随机数,并试图将该数字加入列表中
在生产代码中,我们通常要添加try/finally代码块来保证在异常抛出时也能够将锁释放
Reader-WriterLockSlim还提供了以下属性以监视锁的状态:
有时最好能在一个原子操作中将读锁转换为写锁。
上述操作的问题在于,另一个线程可能会在第3和第4步之间插入并修改链表(例如,添加同一个元素)。而ReaderWriterLockSlim可以通过第三种锁来解决这个问题,称为可升级锁(upgradable lock)
一个可升级锁就像读锁一样,但是它可以在随后通过一个原子操作升级为一个写锁。
从调用者的角度来看,这种操作和嵌套锁或者递归锁很相似。但是,从功能上,第3步中ReaderWriterLockSlim释放读锁并获得一个写锁的操作是原子的
但是一次只能获取一个可升级锁
通常,ReaderWriterLockSlim禁止使用嵌套锁或者递归锁。
递归锁会显著增加代码复杂性,因为它有可能同时获得多种锁
一旦获得了锁,后续的递归锁级别可以更低,但不能更高。其等级顺序如下:读锁→可升级锁→写锁
◆ 22.5 使用事件等待句柄发送信号
最简单的信号发送结构是事件等待句柄(event wait handles)。注意它和C#的事件是无关的
(如果在构造器中以true为参数,则相当于立刻调用Set方法
在AutoResetEvent对象上调用Reset方法可以无须等待或阻塞就关闭闸机的门
假设主线程需要向工作线程连续发送三次信号
因为工作线程需要时间来处理每一次的信号
这是因为Set和WaitOne之间并不是线程安全的
对于这种无限执行的线程,一定要为其设定退出策略
CountdownEvent可用于等待多个线程。
有时,结构化并行(structured parallelism)结构(PLINQ和Parallel类)更易于解决CountdownEvent所能解决的问题
EventWaitHandle构造器可以创建命名的实例以支持跨进程的操作
如果该名称已经被计算机的其他命名实例使用了,那么将返回同一个EventWaitHandle的引用
如果有两个应用程序运行这段代码,那么它们就可以互相发送信号
◆ 22.6 Barrier类
。它允许多个线程在同一时刻汇合。这个类的执行速度很快,非常高效。它是基于Wait、Pulse和自旋锁实现
当需要汇合时,在每一个线程上都调用SignalAndWait
个线程都会打印从0到4的数字,并与其他线程保持步调一致:
创建Barrier对象时还可以指定一个后续操作(post-phase
◆ 22.7 延迟初始化
如何以线程安全的方式初始化一个共享字段是线程编程中的常见问题
从.NET Framework 4.0开始引入了Lazy
类,该类实现了延迟初始化(lazy initialization)功能。如果实例化时以true为参数,则它就会使用上例中线程安全的初始化模式。 双检锁执行一次volatile读操作,避免在对象初始化后进行锁操作
如果在Lazy
构造器中传入false,那么它就会使用非线程安全的延迟初始化模式。
◆ 22.8 线程本地存储
例如消息、事务、安全令牌等
如果将这些数据以方法参数的形式进行传递则代码就会非常难看
而如果将这种数据存储在静态字段中那么它又可以被所有的线程共享而失去独立性。
但是,线程本地存储并不适合在异步代码中使用,因为有一些延续可能会运行在之前的线程上。实现线程本地存储的方法有三种
实现线程本地存储最简单的方式是在静态字段上附加ThreadStatic特性:
ThreadLocal
是.NET Framework 4.0中新增的类型。它对静态和实例字段都提供了线程本地存储支持,并允许指定默认值。 第三种实现线程本地存储的方式是使用Thread类的GetData和SetData方法。
◆ 23.2 PLINQ
PLINQ无法像LINQ那样保持序列的原始顺序。
如果需要保持序列的原始顺序,则必须在AsParallel()之后调用AsOrdered()方法
AsOrdered不是大多数查询的默认行为。
领域驱动设计:软件核心复杂性应对之道(修订版) 埃里克·埃文斯
◆ 第5章 软件中所表示的模型
使用Prolog语言实现的MODEL-DRIVEN DESIGN,它的模型是由逻辑规则和事实构成的
这就是将业务规则引擎或工作流引擎这样的非对象组件集成到对象系统中的动机
大部分系统都必须使用一些非对象的技术基础设施,最常见的就是关系数据库
规则引擎
然而对象范式却缺少用于表达规则和规则交互的具体语义
针对整个系统的全局规则很难应用
逻辑范式
一个常见的结果是应用程序被割裂成两部分
重要的是在使用规则的同时要继续考虑模型
就要完全靠开发人员提炼出一个由清晰的基本概念组成的模型,以便完全支撑整个设计
异构模型
消除两种环境之间的鸿沟
找到适合于范式的模型概念
语言使用上的高度一致性也能防止各个设计部分分裂
将导致人们通过歪曲模型来使它更容易画出来
不能因为存在一些规则,就必须使用规则引擎
规则也可以表示为对象
在决定使用混合范式之前,一定要确信主要范式中的各种可能性都已经尝试过了
◆ 第15章 精炼
它们通过API挂钩(API hook)成功地使用了商用的外部工作流系统
错误日志被深入地集成到应用程序中
选择2:公开发布的设计或模型
◆ 第16章 大型结构
领域的一些重要概念丢失了
严格划分BOUNDED CONTEXT可能会防止出现破坏和混淆
但我们仍然需要理解这些支持性元素,以及它们与CORE DOMAIN的关系
如果没有任何协调机制或规则,那么相同问题的各种不同风格和截然不同的解决方案就会混杂在一起
也不可能看到整个系统的统一视图
结果是开发人员成为各自MODULE的专家
那么开发人员就会陷入“只见树木,不见森林”的境地
我们需要理解各个部分在整体中的角色,而不必去深究细节
大部分大型结构都无法用UML来
表示,而且也不需要这样做
这些大型结构是用来勾画和解释模型及设计的
当团队规模较小而且模型也不太复杂时,只需将模型分解为合理命名的MODULE,再进行一定程度的精炼,然后在开发人员之间进行非正式的协调,以上这些就足以使模型保持良好的组织结构了
但不适当的结构会严重妨碍开发的进展
一些技术架构确实能够解决技术问题,如网络或数据持久化问题
它们往往会妨碍开发人员创建适合于解决特定问题的设计和模型
甚至会妨碍编程语言本身的使用
如果其限定了很多前期设计决策,那么随着需求的变更和理解的深入,这些架构会变得束手束脚
一些技术架构(如J2EE)已经成为主流技术
对领域层中的大型结构却没有做多少研究
在项目前期使用大型结构可能需要很大的成本
甚至会发现先前使用的结构妨碍了我们采取一种使应用程序更清晰和简化的路线
那为什么不去开发应用程序,却在这些架构问题上纠缠不清呢
但如果每次修改都像是一场攻坚战,那么人们很快就会疲乏不堪。
一个没有任何规则的随意设计会产生一些无法理解整体含义且很难维护的系统。
要么就是完全推翻架构而又回到没有协调的开发老路上来
而在于这些规则的严格性和来源
而且还会推动开发在健康的方向上前进,并且保持开发的一致性
不要依此过分限制详细的设计和模型决策
必须在掌握了详细知识之后才能确定
软件需求最佳实践 徐锋著
◆ 1.2 透过表象,分析本质
有的是对原有需求的背离、有的是原有需求的遗漏、有的是业务流程变化引起
说明需求描述与沟通有问题
应该加强需求过程的管理,加强沟通手段的管理
因此应该加强需求捕获方法的组合应用,加强对业务模型的梳理。
需求变更集中在流程间:说明需要采用工作流引擎。
说明需要开发用户界面动态生成器
◆ 1.3 方法论与需求工作
只有全面地了解其局限性才不至于误用、滥用
但是在不同流程之间的数据接口方面还是需要另外的措施
流程块的抽取是十分关键的
因此工作流的分析与抽象是基础,工作流引擎只是实现层的手段,不要认为只要引入工作流引擎就可以解决一切问题
◆ 6.2 周期一:理清框架与脉络
还是要细化步骤吧工作流在实现的过程中重点在于找一个个原子级的流程积木,而工作流引擎只是针对工作任务对这些流程积木进行串接的过程
工作流在实现的过程中重点在于找一个个原子级的流程积木,而工作流引擎只是针对工作任务对这些流程积木进行串接的过程
要点在于能否识别出原子级的流程积木,如果你划出的流程积木被分解了
解决流程变化的问题关键在业务上的梳理
了解未来可能的变化
流程应以产出为中心,而非任务为中心
仅对一个业务活动进行分析是片面的
也就是关键流程产出的人能够更自动自发地执行工作流
任务应该由谁来执行
因为场景的不同,可能会执行不同的流程
SRE:Google运维解密 贝特西·拜尔等
◆ 分布式共识的系统架构模式
一个复制状态机(replicated state machine,RSM)是一个能在多个进程中用同样顺序执行同样的一组操作的系统
共识算法处理节点间对操作顺序的共识
小组中的每个成员在每次提案轮中不一定全部参与了
滑动窗口协议(sliding-window protocol)在RSM成员之间同步状态信息
时间戳在分布式系统中问题非常大
复制多份服务并使用一个唯一的领头人(leader)来进行某种类型的工作是很常见的设计
屏障(barrier)在分布式计算中是一种原语,可以用来阻挡一组进程继续工作
使用屏障实际将一个分
布式计算划分成数个逻辑阶段执行
MapReduce(参见文献[Dea04])可以使用屏障来确保整个Map阶段已经完成,再开始Reduce阶段的计算
屏障也可以用一个RSM系统来实现
锁(lock)是另外一个很有用的协调性原语,可以用RSM实现
避免某个进程崩溃而导致锁无限期被持有
分布式锁是一个应该被小心使用的底层系统原语
队列(queue)是一种常见的数据结构,经常用来给多个工作进程分发任务。
,系统必须保证已经被领取的任务都被成功处理了
利用RSM来实现队列可以将危险性最小化,从而使得整个系统更加可靠。
◆ 简单流水线设计模式与大数据
一个程序的输出作为另外一个程序的输入
多相流水线(multiphase pipeline)
◆ 周期性流水线模式的挑战
周期性的数据流水线
例如MapReduce(参见文献[Dea04])和Flume(参见文献[Cha10])等
◆ 工作分发不均造成的问题
有的时候工作分块所需要的处理资源是不等的,而且理解为什么这一块工作需要更多的资源是很困难的。
record weread notes at at least every week.