【6.824分布式系统笔记】LEC 2: RPC and Threads|Go语言、线程并发、channel 与常见问题

谈论一下Go语言,和接下来的lab中对分布式编程最有用的machinery。

Go内存安全,对线程、锁和同步有良好支持,有一个方便的RPC包。接下来的课程和程序中会经常用到RPC,用来让不同机器之间通信。相比之下C++中线程和内存回收问题极为复杂。

线程是管理并发的主要工具,Go中称为协程(Goroutine),Go中启动入口main函数本身就是一个协程。

使用协程的原因

  • 并发I/O

    Go每个线程可以通过RPC同时对网络上不同服务器发送请求和等待回复。

  • 多核并行

    利用好多核处理器。

  • 后台任务的便利性

    后台进行周期性任务,例如主服务器周期性检查worker是否存活。在之后的lab中会大量用到这个方式。

除了线程,还可以考虑事件驱动编程。它通常是一条线程和一个循环,循环等待可能会触发操作的事件,但是无法使用多核并行能力。而且通常来说,线程并行更方便,但是线程非常多时需要维护每个线程栈和调度表,开销会更大。

或许可以考虑每个线程运行一个事件驱动编程。

写线程代码时会遇到的挑战:

  • 如何处理共享数据

    线程共享地址空间,非原子操作并发时容易出错。此时就需要对非原子操作共享数据加锁

    在编程过程中锁的问题(即线程race)也许不容易发现,Go中提供了race检测工具,在运行时加上 -race 参数即可。

    race检测的原理是跟踪线程近期读写内存位置,发现有不同线程读写同一位置且没有锁时会进行提醒。但是它会使用大量内存,且无法检测静态的未执行代码,使用时需要建立测试标准且让所有代码都执行。

  • 线程协作

    有时候需要线程之间进行交互,例如传输数据。Go中的channel可以实现这一点。

    Sync.conf()也是个好办法,即唤醒信号。

    Sync.waitgroup适合启动已知数量的Goroutines。

  • 线程死锁

    互相等待。在锁设置不合理时容易出现。

实际使用中需要对线程数量进行限制,一种办法是创建固定大小的worker池(线程池),复用线程而不是每次创建一个新线程。

多线程的官方例子:web crawler

网络爬虫的工作:爬取第一个页面,提取所有url,进入url重复这个工作。但是有些url会重复嵌套,所以需要记录已经爬取过的页面。为了提高效率需要并行启动多个爬虫,直到占满带宽。

最难的部分在于,爬虫何时完成结束。

  • 串行爬虫程序:使用递归调用深度优先搜索,维护了一个叫 fetched 的map,记录已经爬过的页面。
  • 一种并行爬虫程序,在读取fetched时需要加锁。**函数传map的引用而不是传值。**使用了waitgroup防止主进程退出。为了防止协程中panic,可以使用defer处理异常(例如出问题后执行defer done防止waitgroup无法退出)。注意waitgroup的done操作本就是有锁的,所以不会出现抢占。
  • 另一种并行程序使用channel来共享信息,就不需要锁。主线程master循环读取channel中的数据,维护fetched表。worker进程爬取url把信息送入channel。这里map只有master进程读写,所以不存在抢占问题。channel内部有锁,也不会发生多线程race问题。

官方代码文件:

crawler.go

kv.go

你可能感兴趣的:(分布式系统,golang,go,分布式,MIT)