微服务学习-day 3-Go Concurrency(1)

1、GoRoutine

Processes and Threads (进程和线程)

操作系统会为该应用程序创建一个进程。作为一个应用程序,它像一个为所有资源而运行的容器。这些资源包括内存地址空间、文件句柄、设备和线程。

线程是操作系统调度的一种执行路径,用于在处理器执行我们在函数中编写的代码。一个进程从一个线程开始,即主线程,当该线程终止时,进程终止。这是因为主线程是应用程序的原点。然后,主线程可以依次启动更多的线程,而这些线程可以启动更多的线程。

无论线程属于哪个进程,操作系统都会安排线程在可用处理器上运行。每个操作系统都有自己的算法来做出这些决定。

Concurrency and Parallelism (并发与并行)

Go语言层面支持的go关键字,可以快速的让一个函数创建为goroutine,我们可以认为main函数就是作为goroutine执行的。操作系统调度线程在可用处理器上运行,Go运行时调度goroutines在绑定到单个操作系统线程的逻辑处理器中运行(P)。即使使用这个单一的逻辑处理器和操作系统线程,也可以调度数十万goroutine以惊人的效率和性能并发运行。

Concurrency is not Parallelism.并发是轮流处理多个任务,并行是同时处理多个任务。)

并发不是并行。并行是指两个或多个线程同时在不同的处理器执行代码。如果将运行时配置为使用多个逻辑处理器,则调度程序将在这些逻辑处理器之间分配goroutine,这将导致goroutine在不同的操作系统线程上运行。但是,要获得真正的并行性,您需要在具有多个物理处理器的计算机上运行程序。否则,goroutines将针对单个物理处理器并发运行,即使Go运行时使用多个逻辑处理器。

Keep yourself busy or do the work yourself

如果你的goroutine在从另一个goroutine获得结果之前无法取得进展,

那么通常情况下,你自己去做这项工作比委托它( go func() )更简单。

这通常消除了将结果从goroutine返回到其启动器所需的大量状态跟踪和chan操作。

Leave concurrency to the caller

把并发的决定权交给调用方,从而防止有隐藏的不可控的GoRoutine出现,确保GoRoutine对于调用方来说,是可见的。

Never start a goroutine without knowning when it will stop

永远不要启动一个你不知道何时会结束的GoRoutine,

启动一个不止到何时会结束的GoRoutine会导致可能出现的GoRoutine泄漏,从而导致内存泄露,服务挂掉

2、Memory Model

https://golang.org/ref/mem

如何保证在一个goroutine中看到在另一个goroutine修改的变量的值,如果程序中修改数据时有其他goroutine同时读取,那么必须将读取串行化。为了串行化访问,请使用channel或其他同步原语,例如sync和sync/atomic来保护数据。

在一个goroutine中,读和写一定是按照程序中的顺序执行的。

即编译器和处理器只有在不会改变这个goroutine的行为时才可能修改读和写的执行顺序。

由于重排,不同的goroutine可能会看到不同的执行顺序。

例如,一个goroutine执行a = 1;b = 2;,另一个goroutine可能看到b在a之前更新。

func main() {
	var a, b int
	go func() {
		time.Sleep(time.Microsecond)
		a = 1
		fmt.Println(b)
	}()
	go func() {
		time.Sleep(time.Microsecond)
		b = 1
		fmt.Println(a)
	}()

	time.Sleep(time.Second)
}

Memory Recording

用户写下的代码,先要编译成汇编代码,也就是各种指令,包括读写内存的指令。

CPU的设计者们,为了榨干CPU的性能,无所不用其极,各种手段都用上了,

你可能听过不少,像流水线、分支预测等等。

其中,为了提高读写内存的效率,会对读写指令进行重新排列,这就是所谓的 内存重排,英文为MemoryReordering。

Store Buffer

现代CPU为了“抚平”内核、内存、硬盘之间的速度差异,搞出了各种策略,例如三级缓存等。

下图:为了让(2)不必等待(1)的执行“效果”可见之后才能执行,我们可以把(1)的效果保存到store buffer

Store Buffer 对于单线程来说是完美的

但是对于多线程,先执行(1)和(3)将他们写入 Store Buffer,接下来(2),(4)去查找变量时发现,它们所在的 CPU 的 Store Buffer 中 并没有对应的变量,于是去内存中拿,从而读出了 0和0.

因此,对于多线程的程序,所有的CPU都会提供“锁”支持,称之为barrier,或者fence。

它要求:barrier(内存屏障)指令要求所有对内存的操作都必须要“扩散”到 memory 之后才能继续执行其他对 memory 的操作。因此,我们可以用高级点的atomiccompare-and-swap ,或者直接用更高级的锁,通常是标准库提供。 Store Buffer

Happen Before (先行发生)

为了说明读和写的必要条件,我们定义了先行发生(Happens Before)。

如果事件 e1 发生在 e2 前,我们可以说 e2 发生在 e1 后。

如果 e1 不发生在 e2 前也不发生在 e2 后,我们就说 e1 和 e2 是并行的。

注意:在单一的独立的 goroutine 中先行发生的顺序即是程序中表达的顺序。

当下面条件满足时,对变量 v 的读操作 r 是被允许看到对 v 的写操作 w 的:
  1. r 不先行发生于 w
  2. 在 w 后 r 前没有对 v 的其他写操
为了保证对变量 v 的读操作 r 看到对 v 的写操作 w ,要确保 w 是 r 允许看到的唯一写操作。即当下面条件满足时,r 被保证看到 w :
  1. w 先行发生于 r
  2. 其他对共享变量 v 的写操作要么在 w 前,要么在 r 后。
这一对条件比前面的条件更严格,需要没有其他写操作与 w 或 r 并行发生。

好好学习,天天向上