1. 可扩展性
1.1. 土耳其的一句谚语:“路到眼前必有车”
1.1.1. “别为还没到来的事情烦恼”
1.2. 单纯的高性能并不能使一个系统具有可扩展性,你需要让所有方面的设计都得能够迎合越来越多的用户
1.3. 没有一个单一的方案可以解决我们所有的问题,我们需要把所有用来解决问题的方法放在我们的工具箱里,根据手头的问题来使用正确的方法
1.4. 从系统的角度来看,提升可扩展性意味着投入更多的硬件来让系统变快
1.5. 从编程的角度来看,可扩展的代码可以在面对日益增长的需求时保持网站的响应速度不变
1.6. 某些代码所能提供的负载是有上限的,而编写可扩展代码的目标就是尽可能地提升这个上限
1.7. 从零开始设计一个完全可扩展的系统是可能的,但实现这一目标所需的努力和时间以及你得到的回报都被“产品需要快速上线”这件事给掩盖了
1.8. 实现可扩展代码的第一步是剥离阻碍实现可扩展的不良代码
1.8.1. 这样的代码会产生瓶颈,导致即使你增加了更多的硬件资源,代码运行仍然缓慢
1.9. 不能让一个CPU核心在1秒内运行比其时钟频率更多的指令
1.10. 增量标识符会泄露你的应用信息
2. 不要使用锁
2.1. 锁定(locking)是一个让你能够写出线程安全代码的特性
2.2. 线程安全(thread safe)的意思是一段代码即使被两个或多个线程同时调用,也能稳定地工作
2.3. 在很多情况下,简单的原子增量操作并不足以使你的代码成为线程安全的
2.4. 你的实例也可能被你的代码之外的一些代码锁定。这可能会导致不必要的延迟甚至死锁,因为你的代码可能在等待其他代码运行完成
2.5. 死锁
2.5.1. 获取资源并等待释放另一个资源结果就像一个无限循环,等待着一个永远不会被满足的条件
2.5.2. 除了清楚地了解代码中的锁机制之外,没有其他解决死锁的“灵丹妙药”,但一个好的经验法则是总是先释放最近获得的锁,并尽快释放
2.5.3. GO编程语言的通道(channel)特性仍然有可能出现死锁,只是可能性比较小
2.6. 确定你使用的共享数据结构是否有无锁的替代方案
2.6.1. 无锁结构可以被多个线程直接访问而不需要任何锁
2.6.2. 无锁结构甚至可能会比有锁的结构慢,但它们的可扩展性会更好
2.6.3. 使用无锁结构的一个常见场景是共享字典(shared dictionary),它在某些平台被称作图(map)
2.6.4. 缓存数据结构时可以使用无锁的设计
2.7. 字典不是线程安全的,但线程安全只在有多个线程修改一个给定的数据结构时才应该考虑
2.8. 所有没有副作用的只读结构都是线程安全的
2.8.1. 如果一个函数对其作用域之外的东西产生了影响,就是产生了副作用
2.8.2. 一个没有任何副作用的函数无论运行多少次,其环境都不会有任何变化
2.8.3. 没有副作用的函数被称为纯函数(pure function)
2.8.3.1. 纯函数有一个好处,它们是100%线程安全的
2.9. .NET提供了两套不同的线程安全数据结构
2.9.1. 以Concurrent开头,使用了“短命”(short-lived)的锁
2.9.1.1. 这套数据结构并不都是无锁的
2.9.1.2. 虽然它们依然使用锁,但它们是被优化过的,锁的持续时间会很短,保证了其速度,而且它们可能比真正的无锁替代方案更简单
2.9.2. Immutable
2.9.2.1. 其中原始数据从未改变,但每个修改操作都会创建一个带有修改内容的新数据副本
2.9.2.2. 在有些情况下,它们可能比Concurrent更合适
2.10. 双重检查的锁
2.10.1. 确保只创建一个实例
2.10.1.1. C#
class Cache {
private static object instanceLock = new object(); ⇽--- 用来锁定的对象
private static Cache instance; ⇽--- 缓存实例值
public static Cache Instance {
get {
lock(instanceLock) { ⇽--- 如果有其他线程在这个代码块里运行,其他所有调用者都会在这等待
if (instance is null) {
instance = new Cache(); ⇽--- 对象被创建,也只被创建一次
}
return instance;
}
}
}
}
2.10.1.2. 对Instance属性的每次访问都会导致它被锁定,这会产生不必要的等待时间
2.10.1.3. 为实例的值添加二次检查(secondary check)
2.10.1.3.1. 如果实例已经被初始化,那么在进行锁定之前返回它的值
2.10.1.3.2. 如果实例还没被初始化,那么只进行锁定
2.10.2. 双重检查锁
2.10.2.1. C#
public static Cache Instance {
get {
if (instance is not null) { ⇽--- 注意C# 9.0中基于模式匹配的“not null”检查
return instance; ⇽--- 返回实例而无须锁定任何内容
}
lock (instanceLock) {
if (instance is null) {
instance = new Cache();
}
return instance;
}
}
}
2.10.2.2. 不是所有的数据结构都可以进行双重检查锁
2.10.2.3. 不能对字典的成员进行双重检查
2.10.2.3.1. 当字典被操作时,不可能在锁之外以线程安全的方式从字典中读取数据
2.10.3. LazyInitializer辅助类
2.10.3.1. 使用LazyInitializer的安全初始化
2.10.3.1.1. C#
public static Cache Instance {
get {
return LazyInitializer.EnsureInitialized(ref instance);
}
}
2.10.3.2. 让安全的单例对象初始化(safe singleton initialization)变得更加容易
2.10.4. 双重检查锁场景的替代方案
2.10.4.1. C#
class LimitedList<T> {
private List<T> items = new();
public LimitedList(int limit) {
Limit = limit;
}
public bool Add(T item) {
if (items.Count >= Limit) { ⇽--- 锁外的第一次检查
return false;
}
lock (items) {
if (items.Count >= Limit) { ⇽--- 锁内的第二次检查
return false;
}
items.Add(item);
return true;
}
}
public bool Remove(T item) {
lock (items) {
return items.Remove(item);
}
}
public int Count => items.Count;
public int Limit { get; }
}