Go语言学习笔记 - 第九章 基于共享变量并发编程(传统)(The Go Programming Language)

第九章 基于共享变量并发编程(传统)

9.1竞争条件

划重点

  • 导出包级别的函数一般情况下都是并发安全的。
  • 。由于package级的变量没法被限制在单一的gorouine,所以修改这些变量“必须”使用互斥条件。
  • 并发调用时没法工作的原因:比如死锁(deadlock)、活锁(livelock)和饿死(resource starvation)
  • 竞争条件指的是程序在多个goroutine交叉执行操作时,没有给出正确的结果
  • 只要有两个goroutine并发访问同一变量,且至少其中的一个是写操作的时候就会发生数据竞争。有三种方法可以避免写竞争:
    • 第一种方法是不要去写变量
    • 第二种避免数据竞争的方法是,避免从多个goroutine访问变量
    • 第三种避免数据竞争的方法是允许很多goroutine去访问变量,但是在同一个时刻最多只有一个goroutine在访问。这种方式被称为“互斥”。
  • Go的口头禅:不要使用共享数据来通信;使用通信来共享数据
  • 一个提供对一个指定的变量通过cahnnel来请求的goroutine叫做这个变量的监控(monitor)goroutine。
  • 一条流水线上的goroutine之间共享变量是很普遍的行为,在这两者间会通过channel来传输地址信息。这种规则有时被称为串行绑定。

常用库及方法

  • image.Image

9.2sync.Mutex互斥锁

划重点

  • 用一个容量只有1的channel来保证最多只有一个goroutine在同一时刻访问一个共享变量。一个只能为1和0的信号量叫做二元信号量(binary semaphore)。
  • 互斥锁(sync.Mutex)。它的Lock方法能够获取到token(这里叫锁),并且Unlock方法会释放这个token。
  • 惯例来说,被mutex所保护的变量是在mutex变量声明之后立刻声明的。如果你的做法和惯例不符,确保在文档里对你的做法进行说明。
var (
mu sync.Mutex // guards balance
balance int
)
  • 在Lock和Unlock之间的代码段中的内容goroutine可以随便读取或者修改,这个代码段叫做临界区。
  • 每一个函数在一开始就获取互斥锁并在最后释放锁,从而保证共享变量不会被并发访问。这种函数、互斥锁和变量的编排叫作监控monitor。
  • 我们用defer来调用Unlock,临界区会隐式地延伸到函数作用域的最后
  • deferred Unlock即使在临界区发生panic时依然会执行,这对于用recover来恢复的程序来说是很重要的
  • defer调用只会比显式地调用Unlock成本高那么一点点,不过却在很大程度上保证了代码的整洁性。
  • 没法对一个已经锁上的mutex来再次上锁–这会导致程序死锁。

常用库及方法

  • sync.Mutex

9.3sync.RWMutex读写锁

划重点

  • 允许多个只读操作并行执行,但写操作会完全互斥。这种锁叫作“多读单写”锁(multiple readers, single writer lock),Go语言提供的这样的锁是sync.RWMutex
  • RLock只能在临界区共享变量没有任何写入操作时可用。
  • RWMutex需要更复杂的内部记录,所以它比一般的无竞争锁的mutex慢一些

常用库及方法

  • sync.RWMutex sync.RWMutex.RLock() sync.RWMutex.RUnlock

9.4内存同步

划重点

  • 如果两个goroutine在不同的CPU上执行,每一个核心有自己的缓存,这样一个goroutine的写入对于其它goroutine的Print,在主存同步之前就是不可见的了。
  • 所有并发的问题都可以用一致的、简单的既定的模式来规避。所以可能的话,将变量限定在goroutine内部;如果是多个goroutine都需要访问的变量,使用互斥条件来访问。

9.5sync.Once初始化

划重点

  • 懒初始化(lazy initialization),可以节省初始化时间
func loadIcons() {
icons = map[string]image.Image{
"spades.png": loadIcon("spades.png"),
"hearts.png": loadIcon("hearts.png"),
"diamonds.png": loadIcon("diamonds.png"),
"clubs.png": loadIcon("clubs.png"),
}
}
// NOTE: not concurrency-safe!
func Icon(name string) image.Image {
if icons == nil {
loadIcons() // one-time initialization
}
return icons[name]
}
  • 上面这个在并发调用是会出现问题,因此,一个goroutine在检查icons是非空时,也并不能就假设这个变量的初始化流程已经走完了,有可能是这样的
func loadIcons() {
icons = make(map[string]image.Image)
icons["spades.png"] = loadIcon("spades.png")
icons["hearts.png"] = loadIcon("hearts.png")
icons["diamonds.png"] = loadIcon("diamonds.png")
icons["clubs.png"] = loadIcon("clubs.png")
}
  • sync.Once是用来解决一次性初始化问题。
  • 一次性的初始化需要一个互斥量mutex和一个boolean变量来记录初始化是不是已经完成了;互斥量用来保护boolean变量和客户端数据结构。

常用库及方法

  • sync.Once sync.Once.Do

9.6竞争条件检测

划重点

  • Go的runtime和工具链为我们装备了一个复杂但好用的动态分析工具,竞争检查器(the race detector)可以有效的帮助我们debug并发程序中的错误。
  • 只要在go build,go run或者go test命令后面加上-race的flag,就会使编译器创建一个你的应用的“修改”版或者一个附带了能够记录所有运行期对共享变量访问工具的test,并且会记录下每一个读或者写共享变量的goroutine的身份信息。
  • 完整的同步事件集合是在The Go Memory Model文档中有说明,该文档是和语言文档放在一起的。

9.7示例: 并发的非阻塞缓存

划重点

  • 并发、不重复、无阻塞的cache,通过channel的广播机制通知其他的goroutine及时读取goroutine的值,广播机制通过close完成

9.8Goroutines和线程

划重点

  • 说goroutine和操作系统的线程区别实际上只是一个量的区别

9.8.1动态栈

划重点

  • 每一个OS线程都有一个固定大小的内存块(一般会是2MB)来做栈,这个栈会用来存储当前正在被调用或挂起(指在调用其它函数时)的函数的内部变量。2MB的栈对于一个小小的goroutine来说是很大的内存浪费,但对于更复杂或者更深层次的递归函数调用来说显然是不够的。一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB,栈的大小会根据需要动态地伸缩,最大值有1GB。

9.8.2Goroutine调度

划重点

  • 一个硬件计时器会中断处理器,这会调用一个叫作scheduler的内核函数。
  • 这里有一段介绍了OS kernel的调度机制,从而引出了goroutine的m:n调度,即在n个操作系统线程上多工(调度)m个goroutine。
  • Go调度器的工作和内核的调度是相似的,但是这个调度器只关注单独的Go程序中的goroutine
  • 操作系统的线程调度不同的是,Go调度器并不是用一个硬件定时器而是被Go语言"建筑"本身进行调度的。
  • 例如当一个goroutine调用了time.Sleep或者被channel调用或者mutex操作阻塞时,调度器会使其进入休眠并开始执行另一个goroutine直到时机到了再去唤醒第一个goroutine。
  • 因为这种调度方式不需要进入内核的上下文,所以重新调度一个goroutine比调度一个线程代价要低得多。

常用库及方法

  • os.Signal signal.Notify os.Interrupt

9.8.3GOMAXPROCS

划重点

  • GOMAXPROCS的变量来决定会有多少个操作系统的线程同时执行Go的代码。
  • 其默认的值是运行机器上的CPU的核心数,所以在一个有8个核心的机器上时,调度器一次会在8个OS线程上去调度GO代码。(GOMAXPROCS是前面说的m:n调度中的n)。
  • 在休眠中的或者在通信中被阻塞的goroutine是不需要一个对应的线程来做调度的。在I/O中或系统调用中或调用非Go语言函数时,是需要一个对应的操作系统线程的,但是GOMAXPROCS并不需要将这几种情况计数在内。
  • 用GOMAXPROCS的环境变量吕显式地控制这个参数,或者也可以在运行时
    runtime.GOMAXPROCS函数来修改它。
  • goroutine的调度是受很多因子影响的,而runtime也是在不断地发展演进的,所以这里的你实际得到的结果可能会因为版本的不同而与我们运行的结果有所不同。

常用库及方法

  • runtime.GOMAXPROCS

9.8.4Goroutine没有ID号

划重点

  • thread-local storage(线程本地存储,多线程编程中不希望其它线程访问的内容)就很容易,只需要以线程的id作为key的一个map就可以解决问题,每一个线程以其id就能从中获取到值,且和其它线程互不冲突。
  • goroutine没有可以被程序员获取到的身份(id)的概念。这一点是设计上故意而为之,由于thread-local storage总是会被滥用。

猜你喜欢

转载自blog.csdn.net/rabbit0206/article/details/103758530