读程序员的制胜技笔记10_可口的扩展

这篇具有很好参考价值的文章主要介绍了读程序员的制胜技笔记10_可口的扩展。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

读程序员的制胜技笔记10_可口的扩展文章来源地址https://www.toymoban.com/news/detail-746160.html

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; }
}

2.10.4.2. 如果不在枚举(enumeration)之前完全锁定列表的话,就不可能在直接索引访问上提供线程安全的枚举

2.10.4.3. 该类只在计算项数时有用,对访问各项没用

2.10.4.4. 访问Count属性操作本身是相当安全的,所以我们可以在双重检查(double-checked)中使用它来获得更好的可扩展性

3. 拥抱不一致

3.1. 平台出于可扩展性的要求,能够容忍一定程度上的不一致

3.2. 数据库提供了大量的特性来避免不一致(inconsistency)

3.2.1. 锁、事务、原子计数、事务日志、页面校验(page checksum)和快照等

3.2.2. 为那些根本不应当检索到错误数据的系统而设计的

3.2.2.1. 银行工作系统、核研究机构及相亲交友软件系统

3.3. 一些不可靠的场景在其他方面,比如性能和可扩展性方面有明显的优势

3.3.1. NoSQL放弃了传统关系数据库系统的某些一致性功能,获得了性能、可扩展性和潜在的回报

3.4. 最终一致性是指你依然能确保一致性,不过需要一些延迟

3.5. 脏数据

3.5.1. 如果一个事务开始向数据库写入一些数据,但写入操作还没完成,这个阶段的数据就被认为是脏数据

3.6. NOLOCK决定了读取它的SQL引擎可能是不一致的,会包含来自尚未提交的事务的数据

3.7. NOLOCK作为查询提示(query hint)出现的原因

3.7.1. SQL中的SELECT查询在表里并不持有锁,但是它仍然会被另一个事务所阻塞

3.7.2. NOLOCK查询提示可以让一个查询读取脏数据(dirty data),但作为回报,它不需要在意其他查询或事务持有的锁

4. 不要缓存数据库连接

4.1. 开启一个单独的数据库连接,并且把它在代码里共享是一个相当普遍的错误操作

4.2. 在运行查询的时候,查询需要不同的事务范围(transaction scope),并且当你试图将单个连接同时用在多个查询时,它们之间会产生冲突

4.3. 当你以为你用的是一个新建立的连接时,其实你用的只是从连接池检索出的一个已经建立的连接

4.4. 以ORM的形式

4.4.1. 现代对象关系映射(object relational mapping,ORM)工具以库的形式存在,可以通过提供一组完全不同的复杂抽象(比如Entity Framework)来掩盖数据库的复杂性

4.5. 你根本不用关心连接的开启与关闭,框架可以自动帮你去处理

4.6. 它在必要时开启连接,并在用完后关闭连接

4.7. 你可以在一个请求的整个生命周期中使用一个与Entity Framework共享的DbContext单个实例

4.8. 你可能不希望在整个应用里使用单个实例,因为DbContext并不是线程安全的

5. 尊重单体

5.1. 微服务背后的逻辑很简单:如果我们把代码分割成单独的自托管项目,那么将来将这些项目部署到独立的服务器会更容易,操作空间很大

5.1.1. 这个逻辑暴露出的问题是增加了复杂性

5.2. 即使只有单个CPU核心,我们也可以实现很好的可扩展性,更不用说在单个服务器上了

5.3. 选择单体架构,也是你本地的代码原型下一步发展的自然结果

5.4. 不要让微服务使新项目复杂化

5.4.1. 只有当使用微服务的优点盖过缺点时才考虑使用微服务

6. 不要使用线程

6.1. 扩展性不仅仅关乎更多的并行化

6.1.1. 它也关乎节约资源

6.1.2. 你不能使用超过现有的全部内存的空间,也不能让CPU占用超过100%

6.2. “抢占”(preemption)

6.2.1. 过去单核CPU的多任务工作方式

6.3. 由于操作系统安排线程的方式,在线程池中拥有比CPU核心数量更多的线程只是一种获得更多CPU利用率的粗糙方法,甚至会损害可扩展性

6.4. 利用等待I/O的时间的一个更准确的方法是使用异步I/O

6.4.1. 异步I/O是非常有前途的

6.5. 异步I/O是很明确的:无论你在哪里有一个wait关键字,都意味着线程将等待回调的结果

6.6. 只要你使用的是一个支持异步调用的框架,将现有的代码升级到异步I/O代码也是很简单的

6.7. 异步函数实际上不需要用后缀Async来命名,但这个惯例可以帮你标识需要你等待的东西

6.7.1. 你必须浏览源代码,找出一个异步函数是否真的是异步的

6.8. 函数声明开头的async关键字只是意味着你可以在这个函数中使用await

6.9. 所有的异步函数必须返回一个Task或者Task<T>

6.9.1. 一个没有返回值的异步函数也可以有一个void返回类型,但这明显会引发一些问题

6.9.2. 没有返回值的异步函数应该总是使用Task来作为返回值

6.10. await关键字的作用是让下一行表达式只有在它前面的表达式运行完毕后才会被执行

6.11. 异步代码的问题

6.11.1. 无I/O,不异步

6.11.1.1. 如果一个函数没有调用异步函数,它就不需要是异步的

6.11.1.2. 异步编程只有在与I/O绑定操作一起使用时才有助于提升代码的可扩展性

6.11.1.3. 你得时刻保持清醒,明白你为什么想把某个函数改写成异步的

6.11.2. 不要把同步和异步混为一谈

6.11.2.1. 在同步代码中调用异步函数的最大问题是,由于异步函数中的其他函数依赖于调用者代码运行完成,它可能会导致死锁,异常处理也可能跟你想的不太一样,因为它将被包裹在一个单独的AggregateException里面

6.11.2.2. 并非每个函数都适合异步。

6.12. 异步多线程

6.12.1. 多线程和异步并不相互排斥,你可以两者兼得

6.12.2. 异步编程可以使一些多线程代码更容易编写,但它既不能完全替代多线程编程,也不能提高代码的可扩展性

6.12.3. 用异步语法编写的多线程代码仍然是普通的多线程代码,它不像异步代码那样节约资源

到了这里,关于读程序员的制胜技笔记10_可口的扩展的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 读程序员的制胜技笔记14_安全审查(下)

    1.2.2.1. 看不出来是什么?那我拒绝为你服务 1.4.1.1. 工作量证明相当消耗客户端的运算资源,对那些性能较低的设备不友好,并且它还会影响设备电池的使用寿命 1.4.1.2. 有可能会严重降低用户体验,其后果甚至比验证码的还要恶劣 3.5.2.1. 存储需求更少,性能更强,数据管理

    2024年02月05日
    浏览(42)
  • 读程序员的制胜技笔记13_安全审查(上)

    5.6.1.1. 任何你不想丢失或泄露的东西都是资产,包括你的源代码、设计文档、数据库、私钥、API令牌、服务器配置,还有Netflix观看清单 5.6.2.1. 每台服务器都会被一些人访问,而每台服务器都会访问其他一些服务器 6.1.1.1. 设计时首先要考虑到安全问题,因为在既有基础上去

    2024年02月05日
    浏览(58)
  • 读程序员的制胜技笔记08_死磕优化(上)

    4.3.1.1. 只能给你一堆用于比较的数字 4.3.1.2. 不能告诉你代码的运行速度是快还是慢 4.3.1.3. 可以告诉你它们比其他一些代码运行得慢还是快 4.3.5.1. 可以消除因测量误差或者调用开销产生的波动 4.3.5.2. 适用于微观基准测试 4.3.5.2.1. 适用于微观基准测试 4.3.5.3. 基准测试并没

    2024年02月05日
    浏览(79)
  • 读程序员的制胜技笔记09_死磕优化(下)

    7.5.3.1. 在256KB之后,提升突然变得杯水车薪

    2024年02月05日
    浏览(70)
  • 读程序员的制胜技笔记02_算法与数据结构

    3.1.1.1. 根据你的需要,可以有更智能的算法 3.1.3.1. 算法本身并不意味着它很聪明 3.2.1.1. public static bool Contains(int[] array, int lookFor) { for (int n = 0; n < array.Length; n++) {        if (array[n] == lookFor) {            return true;        }    }    return false; } 3.3.1.1. public sta

    2024年02月06日
    浏览(58)
  • 读程序员的制胜技笔记12_与Bug共存(下)

    2.2.1.1. 故障代码(failing code)放在一个try语句块里,然后加上一个空的catch语句块,就大功告成了 2.2.1.2. 开发者为整个应用程序添加了一个通用的异常处理程序,但实际上这个程序的工作原理就是忽略所有的异常,也就防止所有的崩溃 2.2.1.3. 如果像那样添加一个空的处理程序

    2024年02月05日
    浏览(53)
  • 读程序员的制胜技笔记11_与Bug共存(上)

    2.7.3.1. 在构造时验证其有效性,这样一来就不可能包含无效值 2.8.2.1. 其主张一个花括号与声明在同一行 2.9.1.1. 看看这些现成的类型 2.9.3.1. 它代表持续时间 2.9.3.2. 你没有理由用TimeSpan以外的任何东西来表示持续时间,即使你所调用的函数不接受TimeSpan作为参数 2.9.4.1. 它也

    2024年02月05日
    浏览(49)
  • 读程序员的制胜技笔记03_有用的反模式(上)

    4.5.4.1. 你在物理数据库结构上增加了一个依赖项 4.5.4.2. 如果你需要改变信息表的布局或所使用的数据库技术,你就必须检查所有的代码,确保所有的东西都能与新的表布局或新的数据库技术一起工作 4.5.6.1. 这是保持组件或整个层尽可能简单的关键 4.8.3.1. 每个成员只对自己

    2024年02月06日
    浏览(43)
  • 读程序员的制胜技笔记04_有用的反模式(下)

    1.3.1.1. 自己做自己的甲方 3.2.2.1. 紧耦合(tight coupling) 3.2.2.2. 依赖性是万恶之源 3.3.7.1. 因为你可能需要用接口而不是具体的引用来定义依赖关系,但这也会使代码摆脱依赖关系 5.2.3.1. 没有其他错误发生时执行的代码部分 5.3.3.1. 退出点(exit point)是指函数中导致其返回给调用

    2024年02月06日
    浏览(80)
  • 10.4K Star!程序员为程序员针对性优化的开源免费笔记

    平时我一直用Notion来记录内容为主,但也一直关注着其他开源产品。上周正好看到一款非常受欢迎的开源免费笔记,今天就推荐给大家: VNote 。 VNote一个由程序员为程序员打造的开源笔记应用,基于Qt开发,专注于使用 Markdown 来写作的群体。它提供完美的编辑体验和强大的笔

    2024年02月12日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包