Go并发优势
Go语言最大的特点就是从语言层面支持并发,开发者不用担心并发的底层逻辑、内存管理,只需要编写好自己的业务逻辑即可。Go语言也提供了十分强大的垃圾回收机制,开发者不用担心创建的量如何销毁。
在其他语言中,编写并发程序往往需要使用其他的并发库才能实现。而在Go语言里,想要编写一个并发程序是非常容易的事情,它不需要额外引用其他的第三方库,只需要使用“go”关键字就可以实现。
在Go语言里,只需要使用"go"加上函数名称就可以让这个函数变为并发函数,如下示例:
package main
func run(arg string) {
// 此线程的任务
}
func main() {
go run("this is new thread")
}
Go语言的并发基于CSP(Communication Sequential Process,通信顺序进程)模型,CSP模型是用于描述两个独立的并发实体通过共享的通信管道(channel)进行通信的并发模型。
CSP中channel是一类对象,它不关注发送消息的实体,而关注发送消息时使用的通信管道。 简单来说,CSP模型提倡通过通信来共享内存,而非通过共享内存来通信。
基于CSP模型,Go语言通过通信的方式,通过安全的通道发送和接收数据以实现同步,避免了显式锁的问题,大大简化了并发编程的编写。
goroutine
goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。
不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。
goroutine定义
在Go语言中,每一个并发的执行单元叫作一个goroutine。我们只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。
go 函数名(函数参数)
一旦我们使用了go关键字,函数的返回值就会被忽略,故不能使用函数返回值来与主线程进行数据交换,而只能使用channel。
线程和协程的区别
协程和线程最重要的区别在于:
- 线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这样代价就小了。
- 协程的切换时间点是由调度器决定,而不是由系统内核决定的,尽管它们的切换点都是时间片超过一定阈值,或者是进入
I/O
或睡眠等状态时。 - 基于垃圾回收的考虑,Go实现了垃圾回收,但垃圾回收的必要条件是内存位于一致状态,因此就需要暂停所有的线程。如果交给系统去做,那么会暂停所有的线程使其一致。但对于Go语言来说,调度器知道什么时候内存位于一致状态,所以也就没有必要暂停所有运行的线程。
线程有固定的栈,基本都是2 MB,都是固定分配的;这个栈用于保存局部变量,在函数切换时使用。但是对于goroutine来说,一个大小固定的栈可能会导致资源浪费,所以Go采用了动态扩张收缩的策略,初始化为2KB,最大可扩张到1GB。
创建goroutine
当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们称之为main goroutine。所有的goroutine在main函数结束时会一并结束。
我们只需要在函数调用语句前面添加go关键字,就可以创建并发执行单元。开发人员无须了解任何执行细节,调度器会自动将其安排到合适的系统线程上去执行。
go 函数名(参数名)
runtime包
Go语言中runtime(运行时)包实现了一个小型的任务调度器。这个调度器的工作原理和系统对线程的调度类似,Go语言调度器可以高效地将CPU资源分配给每一个任务。以下主要介绍三个函数:Gosched()
、Goexit()
、GOMAXPROCS()
。
Gosched()
// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
checkTimeouts()
mcall(gosched_m)
}
Gosched()使当前Go协程放弃处理器,以让其他Go协程运行。它不会挂起当前Go协程,因此当前Go协程未来会恢复执行。
如下示例:
package main
import (
"fmt"
"runtime"
)
func main() {
go func() {
for i := 0; i < 3; i++ {
fmt.Println("go")
}
}()
for i := 0; i < 2; i++ {
runtime.Gosched()
fmt.Println("main")
}
}
运行结果如下:
Go语言的协程是抢占式调度的,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU §转让出去,让其他goroutine能被调度并执行。
一般出现如下几种情况,goroutine就会发生调度:
- syscall
- C函数调用(本质上和syscall一样)
- 主动调用runtime.Gosched
- 某个goroutine的调用时间超过100ms,并且这个goroutine调用了非内联的函数。
Goexit()
// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.
func Goexit()
Goexit()终止调用它的Go协程,但其他Go协程不会受影响。Goexit()会在终止该Go协程前执行所有defer的函数。
如下示例:
package main
import (
"fmt"
"runtime"
"time"
)
func Task1() {
defer fmt.Println("task1 stop")
fmt.Println("task1 start")
fmt.Println("task1 work")
}
func Task2() {
defer fmt.Println("task2 stop")
fmt.Println("task2 start")
runtime.Goexit() // 效果和return一样
fmt.Println("task2 work")
}
func main() {
go Task1()
go Task2()
time.Sleep(time.Second * 5)
}
运行结果如下:
GOMAXPROCS()
GOMAXPROCS(n int)
函数可以设置程序在运行中所使用的CPU数,Go语言程序默认会使用最大CPU数进行计算。
// GOMAXPROCS sets the maximum number of CPUs that can be executing
// simultaneously and returns the previous setting. It defaults to
// the value of runtime.NumCPU. If n < 1, it does not change the current setting.
// This call will go away when the scheduler improves.
func GOMAXPROCS(n int) int
GOMAXPROCS()设置可同时执行的最大CPU数,并返回先前的设置。若n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过NumCPU
查询。
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
n := runtime.GOMAXPROCS(1)
fmt.Println("先前的CPU核数设置为: ", n)
last := time.Now()
for i := 0; i < 100000; i++ {
go func() {
// 耗时任务
a := 999999 ^ 9999999
a = a + 1
}()
}
now := time.Now()
fmt.Println(now.Sub(last))
}
运行结果如下:
当改为12核进行计算时,效率得到了显著的提升,运行结果如下: