Go协程

本文仅是自己阅读笔记,不正确之处请多包涵和纠正。
原文The way to go

一、什么是协程?

1、进程和多线程

一个应用程序是运行在机器上的一个进程;进程是一个运行在自己内存地址空间里的独立执行体。一个进程由一个或多个操作系统线程组成,这些线程其实是共享同一个内存地址空间的一起工作的执行体。几乎所有’正式’的程序都是多线程的,以便让用户或计算机不必等待,或者能够同时服务多个请求或增加性能和吞吐量。

2、多线程的缺点

因为在一个进程里的多个线程是共享内存的,意味着在多线程应用中内存中的数据是被多个线程共享的,它们会被多个线程以无法预知的方式进行操作,导致一些随机的结果。

3、不推荐使用全局变量或者共享内存

使用全局变量或者共享内存会给我们的代码在并发运算的时候带来危险。因为在共享内存数据中,多个线程都可以对数据进行操作,所以我们一般通过锁机制的解决方法来避免发生不可预知的结果。在Go的sync包里也提供了一些工具来在低级别的代码中实现加锁;但是,锁机制往往带来更高的复杂度,使代码变得容易出错并且低性能,所以这个经典的方法明显不再适合现代多核/多处理器编程。

4、协程

(1)协程

在Go中,应用程序并发处理的部分被称作 goroutines(协程),它可以进行更有效的并发运算。在协程和操作系统线程之间并无一对一的关系:协程是根据一个或多个线程的可用性,映射在他们之上的;协程调度器在 Go 运行时很好的完成了这个工作。

(2)协程的工作原理

因为协程是工作在相同的地址空间中,所以共享内存的方式一定是同步的;这个可以通过sync包来实现(不推荐),但是在Go中是使用 channels 来同步协程的

  • 当系统调用(比如等待 I/O)阻塞协程时,其他协程会继续在其他线程上工作。
  • 协程比线程更轻量级,它们只使用少量的内存和资源
  • 协程可以运行在多个操作系统线程之间,也可以运行在线程之内,让你可以很小的内存占用就可以处理大量的任务。由于操作系统线程上的协程时间片,你可以使用少量的操作系统线程就能拥有任意多个提供服务的协程,而且 Go 运行时可以聪明的意识到哪些协程被阻塞了,暂时搁置它们并处理其他协程。
  • 存在两种并发方式:确定性的(明确定义排序)和非确定性的(加锁/互斥从而未定义排序)。Go 的协程和通道理所当然的支持确定性的并发方式。
  • 协程是通过使用关键字 go 调用(执行)一个函数或者方法来实现的。这样会在当前的计算过程中开始一个同时进行的函数,在相同的地址空间中并且分配了独立的栈
  • 任何 Go 程序都必须有的 main() 函数也可以看做是一个协程,尽管它并没有通过 go 来启动。协程可以在程序初始化的过程中运行(在 init() 函数中)。
  • 在一个协程中,比如它需要进行非常密集的运算,你可以在运算循环中周期的使用 runtime.Gosched():这会让出处理器,允许运行其他协程;它并不会使当前协程挂起,所以它会自动恢复执行。使用 Gosched() 可以使计算均匀分布,使通信不至于迟迟得不到响应。

二、并发和并行的差异

Go 的并发原语提供了良好的并发设计基础:表达程序结构以便表示独立地执行的动作;所以Go的重点不在于并行的首要位置:并发程序可能是并行的,也可能不是。并行是一种通过使用多处理器以提高速度的能力。但往往是,一个设计良好的并发程序在并行方面的表现也非常出色。

1、Go的并行

当Golang只是使用一个cpu来执行goroutine的任务,无论启动了多少个协程,这些协程都是并发运行的。**如果想使用多核并行的任务,可以通过设置runtime.GOMAXPROCS()来设置CPU的个数的个数来达到并行执行任务的效果。**亦代表了有多少协程在同时执行。

注意:在Go1.5版本后,GOMAXPROCS默认值已经设置为 CPU的核数,而且对于IO密集型等场景,我们甚至可以把GOMAXPROCS的值超过CPU核数。

2、GOMAXPROCS的使用

gc 编译器真正实现了协程,适当的把协程映射到操作系统线程。使用 gccgo 编译器,会为每一个协程创建操作系统线程。

  • 所有的协程都会共享同一个线程除非将 GOMAXPROCS 设置为一个大于 1 的数。
  • 当 GOMAXPROCS 大于 1 时,会有一个线程池管理许多的线程。通过 gccgo 编译器 GOMAXPROCS 有效的与运行中的协程数量相等。
  • 假设 n 是机器上处理器或者核心的数量。如果你设置环境变量 GOMAXPROCS>=n,或者执runtime.GOMAXPROCS(n),接下来协程会被分割(分散)到 n 个处理器上。
  • 更多的处理器并不意味着性能的线性提升。有这样一个经验法则,对于 n 个核心的情况设置 GOMAXPROCS 为 n-1 以获得最佳性能,也同样需要遵守这条规则:协程的数量 > 1 + GOMAXPROCS > 1
  • 如果在某一时间只有一个协程在执行,不要设置 GOMAXPROCS。
  • GOMAXPROCS 等同于线程数量,在一台核心数多于1个的机器上,会尽可能有等同于核心数的线程在并行运行。

3、主线程和go协程

程序的开启即是主线程开启执行,在一个线程上,可以启动多个协程,所以我们可以知道协程是轻量级的线程(gc 编译器实现协程)。

  • 主线程是一个物理线程,直接作用在 cpu 上的。
  • 当main函数退出时,代表主线程退出了,协程会随着主线程的结束而消亡。

三、用命令行指定使用的核心数量

使用 flags 包,如下:

var numCores = flag.Int("n", 2, "number of CPU cores to use")

在 main() 中:

flag.Parse()
runtime.GOMAXPROCS(*numCores)

协程可以通过调用runtime.Goexit()来停止,尽管这样做几乎没有必要。

四、Go协程中的经典例子

package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("In main()")
	go longWait()
	go shortWait()
	fmt.Println("About to sleep in main()")
	// sleep works with a Duration in nanoseconds (ns) !
	time.Sleep(10 * 1e9)
	fmt.Println("At the end of main()")
}

func longWait() {
	fmt.Println("Beginning longWait()")
	time.Sleep(5 * 1e9) // sleep for 5 seconds
	fmt.Println("End of longWait()")
}

func shortWait() {
	fmt.Println("Beginning shortWait()")
	time.Sleep(2 * 1e9) // sleep for 2 seconds
	fmt.Println("End of shortWait()")
}

输出:

In main()
About to sleep in main()
Beginning longWait()
Beginning shortWait()
End of shortWait()
End of longWait()
At the end of main() //10秒后

若移除 go 关键字,重新运行程序,输出:

In main()
Beginning longWait()
End of longWait()
Beginning shortWait()
End of shortWait()
About to sleep in main()
At the end of main() // 17秒后

main()longWait()shortWait() 三个函数作为独立的处理单元按顺序启动,然后开始并行运行。每一个函数都在运行的开始和结束阶段输出了消息。为了模拟他们运算的时间消耗,我们使用了 time 包中的 Sleep 函数。Sleep() 可以按照指定的时间来暂停函数或协程的执行。

他们按照我们期望的顺序打印出了消息,几乎都一样,以并行的方式。我们让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果我们让 main() 函数停止 4 秒),main() 会提前结束,longWait() 则无法完成。如果我们不在 main() 中等待,协程会随着程序的结束而消亡。
注意:当 main() 函数返回的时候,程序退出:它不会等待任何其他非 main 协程的结束。

你可能感兴趣的:(Golang)