- 摘自jyy课件以及自己笔记
- 对 goroutine介绍 : 线程和协程的结合. 然而我没用过,没啥体会.
如何用好一台计算机?
- 如何用一台 (可靠的) 计算机尽可能多地服务并行的请求
- 关键指标:QPS, tail latency , …
- 我们有的工具
- 线程 (threads)
- 协程 (coroutines)
- 多个可以保存/恢复的执行流 (M2 - libco)
- 比线程更轻量 (完全没有系统调用,也就没有操作系统状态)
数据中心:协程和线程
- 协程:
- 优点:协程间切换开销小。
- 缺点:不受os调度,会有blocking问题;无法利用多处理器
- 一个thread的协程block后,该thread的所有协程都block了。
- 一个thread上即便有多个协程,同一时刻只有一个协程在运行。即 如果只有一个thread,该thread有多个协程,那么只能使用一个cpu核。没做到并行。
- thread:
- 优点:擅长并行,可以利用多处理器。
- 有多少个thread 就能运行在多少核上。(thread number < core number)
- 缺点:占用资源多,thread切换开销大。
- 优点:擅长并行,可以利用多处理器。
Go 和 Goroutine
- Go: 小孩子才做选择,多处理器并行和轻量级并发我全都要!
- Goroutine: 概念上是线程,实际是线程和协程的混合体!
- 每个 CPU 上有一个 Go Worker,自由调度 goroutines
- 执行到 blocking API 时 (例如 sleep, read)
- Go Worker 偷偷改成 non-blocking 的版本
- 成功 → 立即继续执行
- 失败 → 立即 yield 到另一个需要 CPU 的 goroutine
- 太巧妙了!CPU 和操作系统全部用到 100%
- Go Worker 偷偷改成 non-blocking 的版本
- 执行到 blocking API 时 (例如 sleep, read)
- go中可以创建几百万个goroutine 完全没问题
- 如上图 再多的协程 实际上最多并行的也只有T1 T2 T3三个thread
- go 几乎已经把程序运行的开销降到最低了。将CPU切换的时间都省掉了(一个CPU一个thread)
现代编程语言上的系统编程
要理解一点 os课上会讲很多并发算法peterson 生产者-消费者 哲学家 但是实际真正编程时 我们几乎不需要用他们。
那为什么还要学?为了理解。
有原子操作 就可以实现条件变量 有条件变量 就可以实现任何并发算法
正如我们需要学汇编,但实际并不需要写汇编。
Do not communicate by sharing memory; instead, share memory by communicating. ——Effective Go
- 不要用共享内存 也即:不要去设计peterson算法那种并发的协议
- 共享内存 = 万恶之源
- 在奇怪调度下发生的各种并发 bugs
- 条件变量:broadcast 性能低,不 broadcast 容易错
- 信号量:在管理多种资源时就没那么好用了
既然生产者-消费者能解决绝大部分问题,提供一个 API 不就好了?
producer-consumer.go
- 缓存为 0 的 channel 可以用来同步 (先到者等待),而不是用共享内存
当然go里也有共享内存
go 解决的问题
- goroutine 在协程等待的时候切换到另一个协程。(数据中心的IO)
- 并发编程:不用共享内存 而用channel
总结
- 希望每个同学都有一个 “主力现代编程语言”
- Modern C++, Rust, Javascript, …
- 尽管对并发原语和并行性进行了多年的研究,但使用锁进行编程仍然具有挑战性。通常最好将锁隐藏在更高级别的结构中,如同步队列,尽管xv6没有这样做。如果您使用锁进行编程,明智的做法是使用试图识别竞争条件(race conditions)的工具,因为很容易错过需要锁的不变量。
- 大多数操作系统都支持POSIX线程(Pthread),它允许一个用户进程在不同的CPU上同时运行几个线程。Pthread支持用户级锁(user-level locks)、障碍(barriers)等。支持Pthread需要操作系统的支持。例如,应该是这样的情况,如果一个Pthread在系统调用中阻塞,同一进程的另一个Pthread应当能够在该CPU上运行。另一个例子是,如果一个线程改变了其进程的地址空间(例如,映射或取消映射内存),内核必须安排运行同一进程下的线程的其他CPU更新其硬件页表,以反映地址空间的变化。
- 没有原子指令实现锁是可能的,但是代价昂贵,并且大多数操作系统使用原子指令。
- 如果许多CPU试图同时获取相同的锁,可能会付出昂贵的开销。如果一个CPU在其本地cache中缓存了一个锁,而另一个CPU必须获取该锁,那么更新保存该锁的cache行的原子指令必须将该行从一个CPU的cache移动到另一个CPU的cache中,并且可能会使cache行的任何其他副本无效。从另一个CPU的cache中获取cache行可能比从本地cache中获取一行的代价要高几个数量级。
- 为了避免与锁相关的开销,许多操作系统使用无锁的数据结构和算法。例如,可以实现一个像本章开头那样的链表,在列表搜索期间不需要锁,并且使用一个原子指令在一个列表中插入一个条目。然而,无锁编程比有锁编程更复杂;例如,人们必须担心指令和内存重新排序。有锁编程已经很难了,所以xv6避免了无锁编程的额外复杂性。