6.824 Lecture 2 RPC and Threads Notes

Posted on 二 29 六月 2021 in course • 1 min read

1 概要

本课没有涉及分布式系统方面的内容,主要是针对本课程 Lab 使用的编程语言 Go 进行了一个简单的介绍,然后讨论了一下多线程并发相关内容。最后是对一个 Go 写的多线程爬虫代码进行了解读,关注点在并发处理、竞态、锁、多线程协作这块。

本课相关的材料:

  • 课堂 Paper: 本课无论文阅读
  • 课堂录像: https://www.bilibili.com/video/BV1R7411t71W?p=2
  • 课堂 Note: https://pdos.csail.mit.edu/6.824/notes/l-rpc.txt

2 要点

2.1 Why Go

  • Thread (goroutine) 支持
  • Lock: 锁机制应对并发执行和竞态
  • 类型安全
  • 方便的 RPC 库
  • GC 内存安全: 垃圾回收

Go 要比 C++ 更容易使用,语言更简单直接,不会有特别的语法和特性,也不会有那么多奇怪的错误。

2.2 关注并发

对于 Go 来说,并发一般情况是多个 goroutine 在同一个地址空间并发执行。

  • I/O concurrency: 客户端可以请求多个服务端并发等待响应,服务端处理多个客户端的连接请求,在一个请求进行 IO 操作时可以切换处理另外一个请求的计算;

  • Parallelism: 多线程利用多核,实际系统中应该尽量利用所有 CPU 的计算力;

  • Convenience: 后台运行,方便执行处理一些分离的任务;

不用多线程,可以用异步编程 event-driven 的方式:

  • 单线程单 loop;
  • 保存每个状态: 比如请求客户端的状态;
  • 根据事件来执行切换执行任务;
  • 单个运行无法充分利用多核 CPU;
  • 实现相对复杂,使用起来也难以理解

相对大量线程的情况会更优秀,比如有上百万的连接,对应上百万个线程来说,事件驱动更好,节省资源,同时还可以减少线程切换带来的性能损耗。实现上通常可以多个线程,每个线程都有个独立的事件循环来执行任务,这样可以利用多核资源。比如 Nginx,是基于多 Worker 线程的事件驱动模型来实现高性能并发处理大量请求的支持。

2.3 多线程的挑战

共享数据、竞态数据: 多线程访问处理容易出现 bug,并发更新可能会出现问题,机器操作可能不是原子指令

  • 需要使用锁来解决这个问题;
  • 或者避免共享可变数据;

coordination 多线程协作执行

  • channels
  • sync.Cond
  • waitGroup

死锁

  • 锁或者 channel 误用,出现彼此依赖释放或者消费的情况,导致了死锁;

2.4 爬虫示例

示例代码主要是实现模拟爬虫处理页面抓取的功能,需要考虑以下内容:

  • 一个页面可能还包含了其他的页面 URL
  • 多个页面可能包含同一个 URL,不应该重复抓取
  • 多个页面直接包含 URL 可能会构成一个环
  • 页面抓取应当并发进行,可以加速整个任务的执行

课堂上主要是介绍了两个版本的并发抓取爬虫:

  1. 基于锁的并发爬虫
  2. 每个发现的 URL 都创建一个抓取页面的线程
  3. 多个线程之间共享一个 map 数据来记录已经抓取到的页面,避免重复和循环抓取
  4. 多个线程对共享的 map 数据操作时需要加锁,避免出现竞态并发更新/读取,在 Go 这会导致 panic 或者内部数据异常
  5. 可以通过 go 编译器自身的 -race 工具来检查代码中的竞态问题
  6. 基于 Channel 的并发爬虫
  7. 区分为 Master 和 Worker 线程
  8. Master 线程创建 Worker 线程去抓取单个页面
  9. Master 和 Worker 线程之间共享一个 channel,Worker 把抓取到的页面里面包含的 URL 发送到这个 channel;
  10. Master 记录 Worker 执行抓取过的 URL,从 channel 获取到新的页面,先检查是否已经抓取过,如果没有则启动新的 Worker 线程抓取,有则跳过;

基于 channel 不需要加锁,是因为记录抓取过页面的 map 数据实际上没有在多个线程中共享,也不存在多线程并发读取更新的情况。但是实际上,channel 数据结构本身在 Go 的实现应该是存在着锁的,这样多个线程每次只有一个线程可以把 URL 发送到 channel 中。

3 总结

本课内容相对简单,Go 语言对于并发的支持比较好,提供了方便的线程(goroutine) 启动方式,此外还对多线程间的协作提供了包括 channel 、sync 等工具来支持。课程原本是用 C++ 来实现 Lab 相关的编码的,近些年在 Go 语言成熟起来后就切换了。使用 Go 来学习和实现分布式系统,可以让学生更关注分布式系统本身相关的内容,而不是在 C++ 的语言特性和代码 Bug 中花费大量的时间。

Go 语言本身也比较适合网络编程,在业界中有不少的成熟的分布式系统实现,比如 etcd、TiDB、Kubernetes 等。