Go语言的并发通过goroutine实现。goroutine类似于线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言运行时调度完成,而线程是由操作系统调度完成。

Go语言还提供channel在多个goroutine间进行通信。goroutine和channel是 Go语言秉承的 CSP(Communicating Sequential Process)并发模式的重要实现基础。

goroutine

在Go语言编程中不需要去自己写进程、线程、协程,当我们需要让某个任务并发执行的时候,只需要起一个goroutinue就可以了。Go程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必定对应一个函数。

创建goroutine

  1. func hello() {
  2. fmt.Println("Hello Goroutine!")
  3. }
  4. func main() {
  5. go hello()
  6. fmt.Println("main goroutine done!")
  7. }
  8. // OUT
  9. // main goroutine done!

在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main()函数返回的时候该goroutine就结束了,所有在main()函数中启动的goroutine会一同结束,因此hello()函数尚未执行

同步——sync.WaitGroup

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加Add(N)。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些时间,而此时mian函数所在的goroutine是继续执行的.

需要注意sync.WaitGroup是一个结构体,传递的时候要传递指针。

调度

OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(调度m个goroutine到n个OS线程)。goroutine的调度不需要切换内核语境,所以调用一个goroutine比调度一个线程成本低很多。

Go运行时的调度器使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码。默认值是机器上的CPU核心数。例如在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCS是m:n调度中的n)。

Go语言中可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。使用格式如下:runtime.GOMAXPROCS(n)

Go语言中的操作系统线程和goroutine的关系:

\1. 一个操作系统线程对应用户态多个goroutine。

\2. go程序可以同时使用多个操作系统线程。

\3. goroutine和OS线程是多对多的关系,即m:n。

channel

虽然可以使用共享内存进行数据交换,但是共享内存在不同的goroutine中容易发生竞态问题。为了保证数据交换的正确性,必须使用互斥量对内存进行加锁,这种做法势必造成性能问题。

go语言的并发模型是CSP,提倡通过通信共享内存而不是通过共享内存而实现通信

如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个goroutine发送特定值到另一个goroutine的通信机制。

Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

声明channel

声明通道类型的格式如下:

var 变量 chan 元素类型

创建channel

通道是引用类型,通道类型的空值是nil。

声明的通道后需要使用make函数初始化之后才能使用。 创建channel的格式如下:

make(chan 元素类型, [缓冲大小]) //缓冲大小是可选的。

channel操作

通道有发送(send)、接收(receive)和关闭(close)三种操作。

发送和接收都使用 <- 符号。

发送

将一个值发送到通道中。

ch <- 10 // 把10发送到ch中

接收

从一个通道中接收值。

x := <- ch // 从ch中接收值并赋值给变量x

<-ch // 从ch中接收值,忽略结果

关闭

我们通过调用内置的close函数来关闭通道。

close(ch)

关于关闭通道需要注意的事情是,只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被垃圾回收机制回收的,它和关闭文件是不一样的,在结束操作之后关闭文件是必须要做的,但关闭通道不是必须的。

关闭后的通道有以下特点:

对一个关闭的通道再发送值就会导致panic。

对一个关闭的通道进行接收会一直获取值直到通道为空。

对一个关闭的并且没有值的通道执行接收操作会得到对应类型的零值。

关闭一个已经关闭的通道会导致panic。

无缓冲的通道

无缓冲的通道只有在有人接收值的时候才能发送值

简单来说就是无缓冲的通道必须有接收才能发送。

解决方法:启用一个goroutine去接收。无缓冲通道上的发送操作会阻塞,直到另一个goroutine在该通道上执行接收操作,这时值才能发送成功,两个goroutine将继续执行。相反,如果接收操作先执行,接收方的goroutine将阻塞,直到另一个goroutine在该通道上发送一个值。

使用无缓冲通道进行通信将导致发送和接收的goroutine同步化。因此,无缓冲通道也被称为同步通道。

有缓冲的通道

只要通道的容量大于零,那么该通道就是有缓冲的通道,通道的容量表示通道中能存放元素的数量。就像你小区的快递柜只有那么个多格子,格子满了就装不下了,就阻塞了,等到别人取走一个快递员就能往里面放一个。

我们可以使用内置的len函数获取通道内元素的数量,使用cap函数获取通道的容量。

单向通道

其中,chan<- int是一个只能发送的通道,可以发送但是不能接收;<-chan int是一个只能接收的通道,可以接收但是不能发送。在函数传参及任何赋值操作中将双向通道转换为单向通道是可以的,但反过来是不可以的。