Go语言并发简介(并发与并行的区别)
Go 语言中的并发是指能让某个函数独立于其他函数运行的能力。
当一个函数创建为 goroutine(协程)时,Go语言会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。
并行就是在任一粒度的时间内都具备同时执行的能力:最简单的并行就是多机,多台机器并行处理;SMP 表面上看是并行的,但由于是共享内存、线程间的同步等,不可能完全做到并行。
并发是在规定的时间内多个请求都得到执行和处理,强调的是给外界的感觉,实际上内部可能是分时操作的。并发重在避免阻塞,使程序不会因为一个阻塞而停止处理。并发典型的应用场景是分时操作系统。
并行是硬件和操作系统开发者重点考虑的问题,作为应用层的程序员,唯一可以选择的就是充分借助操作系统提供的 API 和程序语言特性,结合实际需求设计出具有良好并发结构的程序,提升程序的并发处理能力。现代操作系统能够提供的最基础的并发模型就是多线程和多进程。
在当前的计算机体系下,并行具有瞬时性,并发具有过程性;并发在于结构,并行在于执行。应用程序具备好的并发结构,操作系统才能更好地利用硬件并行执行,同时避免阻塞等待,合理地进行调度,提升 CPU 利用率。应用层程序员提升程序并发处理能力的一个重要手段,就是为程序设计良好的并发结构。
例如:
程序按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,并且是并行的方式。让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果让 main() 函数停止 4 秒),main() 会提前结束,longWait() 则无法完成。如果不在 main() 中等待,协程会随着程序的结束而消亡。
当 main() 函数返回的时候,程序退出,它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。
另外,协程是独立的处理单元,一旦陆续启动一些协程,就无法确定它们是什么时候真正开始执行的。代码逻辑必须独立于协程调用的顺序。
并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情。
在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go 语言设计的哲学。
如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。
不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Go 语言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。
当一个函数创建为 goroutine(协程)时,Go语言会将其视为一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。
并发与并行
并发是指在同一时间段内执行多个任务。并行是指在同一时刻执行多个任务。并行就是在任一粒度的时间内都具备同时执行的能力:最简单的并行就是多机,多台机器并行处理;SMP 表面上看是并行的,但由于是共享内存、线程间的同步等,不可能完全做到并行。
并发是在规定的时间内多个请求都得到执行和处理,强调的是给外界的感觉,实际上内部可能是分时操作的。并发重在避免阻塞,使程序不会因为一个阻塞而停止处理。并发典型的应用场景是分时操作系统。
并行是硬件和操作系统开发者重点考虑的问题,作为应用层的程序员,唯一可以选择的就是充分借助操作系统提供的 API 和程序语言特性,结合实际需求设计出具有良好并发结构的程序,提升程序的并发处理能力。现代操作系统能够提供的最基础的并发模型就是多线程和多进程。
在当前的计算机体系下,并行具有瞬时性,并发具有过程性;并发在于结构,并行在于执行。应用程序具备好的并发结构,操作系统才能更好地利用硬件并行执行,同时避免阻塞等待,合理地进行调度,提升 CPU 利用率。应用层程序员提升程序并发处理能力的一个重要手段,就是为程序设计良好的并发结构。
指定使用核心数
Go 语言会默认使用 CPU 的核心数,这些功能都是自动调整的,但是也提供了相应的标准库来指定核心数。使用 flag 包可以调整程序运行时调用的 CPU 核心数。例如:
var numCores = flag. Int("n", 2, "CPU核心数") in main() flag.Pars() runtime.GOMAXPROCS(*numCores)协程可以通过调用 runtime.Goexit() 来停止,尽管这样做几乎没有必要。
package main import ( "fmt" "time" ) func main() { fmt.printin("这里是main()开始的地方") go longWait() go shortwait() fmt.Println("挂起main()") //挂起工作时间以纳秒(ns)为单位 time.Sleep(10 * 1e9) fmt.Println("这里是main()结束的地方") } func longWait() { fmt.Println("开始longWait()") time.Sleep(5 * 1e9) //等待5秒 fmt.Println("结束longWait()") } func shortWait() { fmt.Println("开始shortWait()" ) time.Sleep(2 * 1e9) //等待2秒 fmt.Println("结束shortwait()") }运行结果为:
这里是main()结束的地方
挂起main()
开始shortWait()
开始longWait()
结束shortwait()
结束longWait()
这里是main()结束的地方
程序按照我们期望的顺序打印出了消息,几乎都一样,可是我们明白这是模拟出来的,并且是并行的方式。让 main() 函数暂停 10 秒从而确定它会在另外两个协程之后结束。如果不这样(如果让 main() 函数停止 4 秒),main() 会提前结束,longWait() 则无法完成。如果不在 main() 中等待,协程会随着程序的结束而消亡。
当 main() 函数返回的时候,程序退出,它不会等待任何其他非 main 协程的结束。这就是为什么在服务器程序中,每一个请求都会启动一个协程来处理,server() 函数必须保持运行状态。通常使用一个无限循环来达到这样的目的。
另外,协程是独立的处理单元,一旦陆续启动一些协程,就无法确定它们是什么时候真正开始执行的。代码逻辑必须独立于协程调用的顺序。
并发与并行的区别
并发和并行的根本区别是任务是否同时执行。并发不是并行。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情。
在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go 语言设计的哲学。
如果希望让 goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将 goroutine 平等分配到每个逻辑处理器上。这会让 goroutine 在不同的线程上运行。
不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕 Go 语言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。