分布式系统学习——MIT6.824-Lecture2课堂笔记

课堂笔记

Go 语言

Why use Go

很多系统风格的语言可以选择,比如 Java、C#、Python、C++。Go 像其它语言一样,提供了很多许多 features,比如 thread 线程、locking、synchronization,尤其是 RPC(remote procedure call)包,非常方便实用。

与 C++不同,Go Type safe and memory safe,编程内存问题会少很多,垃圾回收机制也会帮助我们进行内存管理避免错误。Combination of threads and garbage collection is particularly important。没有如此特性的 C++,需要找出最后一个线程利用完对象后再 free,但这很困难,也很麻烦。

Go 还很简单,比起 C++更难出错,编译器给出的错误提示也更有帮助,C++还要去想 error message 实际表示哪里出错了。

Threads

多线程,我们进行并发编程最常用到的。并发性是分布式程序中非常重要的特性。需要一种更简单的方式进行一对多的交流。go routinesthreads一码事。

一个 Box 表示的 Address space 地址空间。

  • 串行程序:1 个 pc、1 组寄存器、1 个栈
  • 并行程序:每一个程序都有一个 separate pc、寄存器和栈。

实际上串行程序的栈等都集中在一个地址空间。

使用 Threads 的主要原因

IO concurrency

使用这个称呼是来源于单机的并发(同时等待或操作 IO),在分布式中,用来描述处理请求(launched or removed RPCs)的同时等待着其他机器的回复(waiting for many replies)。使用 Threads,可以让其中一部分等待,一部分执行。

Multi-core parallelism

多核机器,在不同核上进行计算会提供 Threads 物理上真正的并行运行。

Convenience

A little bit less important

一些工作不需要始终 running,只是阶段性的进行,比如 Master Server 检查其他 Workers 是否 alive,间隔固定实践发送某些消息。

Q&A

Q:Threads 所带来的 overhead 是否值得? A:负担是有的,但是很多线程不是持续运行,或许经常 Sleep,所以 Probably 负担不大。(后面听不懂了)

Q:提到 asynchronous program(教授称为 eventdriven programming,事件驱动编程) A:如果不想采用 threads 的方式,却要满足一台 server 同时与多台 clients 交流的需求?那么可以采用 asynchronous program 或者 event-driven programing。单一 thread,单一循环,以不同事件相应。编程比较方便,不能解决的问题。无法达到 parallelism 的特性,单一 loop 并不能完全体现出多核机器的优势。不过通常来说其 overhead 要比多线程编程小一些。

为什么说 threads 的 overhead 略大,虽然线程都是轻量级的,但每一个线程都有自己的栈,如果有百万个线程,那代价是庞大的。

Q:Threads 与 Process 的区别,即线程与进程的区别 A:Threads inside process,是操作系统的概念了,unix process 具备一个大的地址空间,而线程(threads、go routines 在其内部),一些操作系统并不 care 线程内部用的编程语言?操作系统级会将不同进程的地址空间隔离开,不同 processes 之间的交流是极少的。同一进程内部的线程可以共享一些内存空间,或者根据 channel 进行同步,或者 mutex 信号量。进程间达不到这样的能力。

Q:带线程的进程间 switch 是怎么操作的? A:比较复杂。可能会从中选择一个。

一个最主要特点,share memory。同时也容易产生 bug。

Race 问题

以 n=n+1 为例,多个线程同时进行,可能答案并不正确。RACE 名字是源于,后来的 Process 是否能看到先来的 Process 的修改,像一场赛跑。(ATOMIC 涉及到指令涉及),Go 语言提供的锁机制(mu.lock() mu.unlock())可以帮助我们“锁住”共享变量。

Q:Go 如何知道锁住哪些变量,mutex 变量和需要被“锁”的变量有哪些联系 A:实际上,Go have no idea.(hhhh)。这是编程人员的工作,keep in head the relationship to the data。变量相互间结构是复杂的(Programming language),里面可能携带有多个锁,根据操作可能会 allowcate 新的 data structure,这都要编程人员自己去想。

Coordination 问题

方便线程之间交流协调,Go 提供了下面的工具

  • channels:收发信息
  • sync.Cond:条件锁,更好地知道是要条件性等待,还是继续执行线程
  • wait group:lauch 一组线程并等待完成

Deadlock 问题

老生常谈,死锁。

例子:Web Crawler

爬虫会启动很多个线程进行 URLs 的爬取,希望知道爬取的 URLs 是刚刚 fetch for 还是已经完成,爬虫希望避免重复爬取。Serial fetch 时间很长,我们希望 Parallel fetch,一个最终的问题,如何确定爬虫停止,获取全部的爬虫信息。

提供的 serial fetch,采取 DFS 爬取树形网页结构,并且用 map、set 储存状态。只有一个 fetch map,递归的 fetch 通过传递该 map 的引用(copy 代价太大了)来确定状态。

Serial Crawler

迅速建立一批 go routines 就停止了。

Concurrent Crawler

感觉 wait group 实现用到了整型信号量的思路)加入了 mutex 和 wait group,可以保证 routines 不会 fetch 相同的 url, 如果不加入 42-45 行锁,当两个线程同时尝试两个 url 时候,会同时设定为 true,从而导致重复 fetch。所以读共享 table 操作要加入锁。

!的确应该用defer done.Done(),否则不能保证函数调用执行完后再释放。

Q:如何保证 done.Done 操作不会产生 Race 问题? A:有内部锁保证。(可以理解成 sync.WaitGroup 操作都是线程安全的)。

关于变量 u 的解释没太听懂。

Q:如果 inner function,作为 go routine 启动并且 refer 了 surrounding function 的变量,那当 surrounding function 返回时,refer 的变量怎么办? A:答案时 Go 的实现会意识外部的 closure 并会分析,编译器会分配heat memory(热内存)来 hold 住这些变量的值,从栈中转移到堆中,这样 inner function 仍能到他们,最后垃圾处理机制会处理这些变量。

编程中 race 问题是常见的

Go 提供了优秀的工具,race 检测工具go run -race,检测不同 routines 先读后写又无锁的情况。

Q:静态分析? A:并不是,race editor 并不是静态分析,是对 run 过程中进行的检测。

一共多少并发线程可以同时运行

样例中看起来 5,实际工程中并没有 limit

Concurrent Channel

使用 Channel 而不是共享变量的形式,channel 是线程安全的(Q&A 里顺便提了一下)。感觉这种形式有点像事件驱动?

  • worker 控制 channel 压入(urls 或者 strings 切片)
  • master 是个 loop 对 channel 弹出,根据弹出内容执行新建 worker,还是其他。