并发指在同一时间内可以执行多个任务。
Go语言通过编译器运行时(runtime),从语言上支持了并发的特性。Go语言的并发通过goroutine特性完成。goroutine类似于线程,但是可以根据需要创建多个goroutine并发工作。goroutine是由Go语言的运行时调度完成的,而线程是由操作系统调度完成。
使用者分配足够多的任务,系统能自动帮助使用者把任务分配到CPU上,让这些任务尽量并发运作。这种机制在Go语言中成为goroutine。
Go程序从main包的main()函数开始,在程序启动时,Go程序就会为main()函数创建一个默认的goroutine.
Go程序中使用go关键字为一个函数创建一个goroutine。一个函数可以被创建多个goroutine,一个goroutine必定对应一个函数。
go 函数名(参数列表)
函数名:要调用的函数名
参数列表:调用函数传入的参数
使用go关键字创建goroutine时,被调用函数的返回值会被忽略。
如果需要在goroutine中返回数据,请使用channel。
func foo(a int) {
//todo
}
go foo(12) //创建goroutine
go func(参数列表) {
//todo
}(调用参数列表)
调用参数列表:启动goroutine时,需要向匿名函数传递的调用参数
注意:
所有goroutine在main()函数结束时一同结束
在Go程序运行时(runtime)实现了一个小型调度器。这套调度器的工作原理类似操作系统调度线程。Go程序调度器可以高效地将CPU资源分配给每一个任务。
//设置逻辑CPU的数量
runtime.GOMAXPROCS(逻辑CPU数量)
逻辑CPU数量数量可以为如下几种数值:
<1 :不修改任何数值
=1 :单核心执行
>1 :多核并发执行
//查询CPU数量
runtime.NumCPU()
runtime.GOMAXPROCS(runtime.NumCPU())
Go语言在GOMAXPROCS数量与任务数量相等时,可以做到并行执行,但一般情况下都是并发执行。
并发(concurrency):把任务在不同的时间点交给处理器进行处理。在同一时间点,任务不会同时运行。
并行(parallelism):把每一个任务分配给每一个处理器独立完成。在同一时间点,任务一定是同时运行。
goroutine和coroutine的区别在于,goroutine可能发生并行执行,而coroutine始终顺序执行。
Go语言中的通道(channel)是一种特殊的类型。在任何时候,同时只能有一个goroutine访问通道进行发送和获取数据。goroutine间通过通道就可以通信。通道就像一个队列,总是遵循先入先出的规则,保证收发数据的顺序。
var 通道变量 chan 通道类型
通道类型:通道内的数据类型
通道变量:保存通道的变量
chan类型的空值是nil,声明后需要配合make才能使用
通道是引用类型,需要使用make进行创建。
通道实例 := make(chan 数据类型)
举例:
var ch1 chan int = make(chan int)
var ch2 = make(chan int)
ch3 := make(chan int)
通道创建后,就可以使用通道进行发送和接收操作。
通道变量 <- 值
通道变量:使用make创建好的通道实例
值:值的类型必须与通道的元素类型一致
例:
ch := make(chan interface{}) //通道可存放任何数据类型
ch <- 0
ch <- "hello"
把数据往通道中发送时,如果接收方一直都没有接收,那么发送操作将持续阻塞。Go程序运行时能智能地发现一些永远无法发送成功的语句并报错。
●通道的收发操作在不同的两个goroutine间进行。
由于通道的数据在没有接收方处理时,数据发送方会持续阻塞,因此通道的接收必定在另外一个goroutine中进行。
●接收将持续阻塞直到发送方发送数据
如果接收方接收时,通道中没有发送方发送数据,接收方也会阻塞,直到发送方发送数据为止。
●每次接收一个元素。
通道一次只能接收一个数据元素。
data := <-ch
执行该语句时将会发生阻塞,直到接收到数据并赋值给data变量
data, ok := <-ch
data:表示接收到的数据。未接收到数据时,data为通道类型的零值。
ok:表示是否接收到数据
使用非阻塞方式从通道接收数据时,语句不会发生阻塞。
阻塞接收数据后,忽略从通道返回的数据。
<-ch
对于通道的接收和发送,我们用生活中一个简单的例子来总结说明一下。通道的接收和发送类似于我们生活中送外卖的过程,骑手把外卖送到目的地的过程,就是把数据放入通道的过程。如果外卖没有被取走,即通道中的数据没有被取走,那么骑手(发送方)就会一直呆在原地等待(阻塞),直到外卖(数据)被取走。同理,点外卖的人就是接收方,在下单以后就要一直等待(阻塞),直到外卖到以后才会解除阻塞。
通道的数据接收可以借用for range语句进行多个元素的接收操作,格式如下:
for data := range chs {
}
通道ch是可以进行遍历的,遍历的结果就是接收到的数据。数据类型就是通道的数据类型。
Go的通道可以在声明时约束其操作方向,如只发送或只接收。这种被约束方向的通道被称作单向通道。
var 通道实例 chan<- int //只能发送通道
var 通道实例 <-chan int //只能接收通道
在无缓冲通道的基础上,为通道增加一个有限大小的存储空间形成带缓冲通道。带缓冲通道在发送时无需等待接收方接收即可完成发送过程,并且不会发生阻塞,只有当存储空间满时才会发生阻塞。同理,如果缓冲通道中有数据,接收时将不会发生阻塞,直到通道中没有数据可读时,通道将会再度阻塞。
无缓冲通道保证收发过程同步。无缓冲收发过程类似于快递员给你电话让你下楼取快递,整个递交快递的过程是同步发生的,你和快递员不见不散。但这样做快递员就必须等待所有人下楼完成操作后才能完成所有投递工作。如果快递员将快递放入快递柜中,并通知用户来取,快递员和用户就成了异步收发过程,效率可以有明显的提升。带缓冲的通道就是这样的一个“快递柜”。
通道实例 := make(chan 通道类型, 通道大小)
通道类型:和无缓冲通道用法一致
通道大小:缓冲大小,决定通道最多可以保存的元素数量
通道实例:被创建的通道实例
带缓冲通道在很多特性上和无缓冲通道是类似的。无缓冲通道可以看做是长度为0的带缓冲通道。
带缓冲通道被填满时,尝试再次发送数据时发送阻塞。
带缓冲通道为空时,尝试接收数据时发生阻塞。
多路复用是通信和网络中的一个专业术语。多路复用通常表示一个信道上传输多路信号或数据流的过程和技术。
Go语言中提供了select关键字,可以同时相应多个通道的操作。select的每个case都会对应一个通道的收发过程。当收发完成时,就会触发case中响应的语句。多个操作在每次select中挑选一个进行响应。
select{
case 操作1:
响应操作1
case 操作2:
响应操作2
…
default:
没有操作情况
}
通道是一个引用对象,和map类似。map在没有任何外部引用时,Go程序在运行时(runtime)会自动对内存进行垃圾回收(Garbage Collection,GC)。类似的,通道也可以被垃圾回收,但是通道也可以被主动关闭。
使用close()关闭一个通道,关闭的通道依然可以被访问,访问被关闭的通道可能会发生问题。
close(ch)
被关闭的通道不会被置为nil。如果尝试对已经关闭的通道进行发送,将会发生宕机。
package main
import "fmt"
func main() {
// 创建一个整型的通道
ch := make(chan int)
// 关闭通道
close(ch)
// 打印通道的指针, 容量和长度
fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))
// 给关闭的通道发送数据
ch <- 1
}
运行结果:
panic: send on closed channel
如果已经关闭的通道中还有数据,这些数据不会被释放,我们可以从通道中读取出来;如果已关闭的通道没有数据,也可以读取,但是读取出来的是通道数据类型的零值。
package main
import "fmt"
func main() {
// 创建一个整型带两个缓冲的通道
ch := make(chan int, 2)
// 给通道放入两个数据
ch <- 0
ch <- 1
// 关闭缓冲
close(ch)
// 遍历缓冲所有数据, 且多遍历1个
for i := 0; i < cap(ch)+1; i++ {
// 从通道中取出数据
v, ok := <-ch
// 打印取出数据的状态
fmt.Println(v, ok)
}
}
运行结果:
0 true
1 true
0 false
互斥锁是一种常用的控制共享资源访问的方法,它能够保证同时只有一个goroutine可以访问共享资源。
package main
import (
"fmt"
"sync"
)
var (
// 逻辑中使用的某个变量
count int
// 与变量对应的使用互斥锁
countGuard sync.Mutex
)
func GetCount() int {
// 锁定
countGuard.Lock()
// 在函数退出时解除锁定
defer countGuard.Unlock()
return count
}
func SetCount(c int) {
countGuard.Lock()
count = c
countGuard.Unlock()
}
func main() {
// 可以进行并发安全的设置
SetCount(1)
// 可以进行并发安全的获取
fmt.Println(GetCount())
}
在读多写少的环境中,优先使用读写互斥锁,它比互斥锁更加高效。原因是,如果多个goroutine同时读时不会阻塞。
var (
// 逻辑中使用的某个变量
count int
// 与变量对应的使用互斥锁
countGuard sync.RWMutex
)
func GetCount() int {
// 锁定
countGuard.RLock()
// 在函数退出时解除锁定
defer countGuard.RUnlock()
return count
}
除了可以使用通道和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务。
方法名 | 功能 |
(wg *WaitGroup) Add(delta int) | 等待组的计数器+1 |
(wg *WaitGroup) Done() | 等待组的计数器-1 |
(wg *WaitGroup) Wait() | 当等待组计算器不等于0时阻塞直到变为0 |
等待组内部拥有一个计数器,计数器的值可以通过方法调用实现计数器值的增加和减少。当我们添加了N个并发任务进行工作时,将等待组的计数器值增加N。每个任务完成时,这个值减1.同时,在另外一个goroutine中,等待这个等待组的计数器值为0,表示所有任务已经完成。
package main
import (
"fmt"
"net/http"
"sync"
)
func main() {
// 声明一个等待组
var wg sync.WaitGroup
// 准备一系列的网站地址
var urls = []string{
"http://www.github.com/",
"https://www.qiniu.com/",
"https://www.golangtc.com/",
}
// 遍历这些地址
for _, url := range urls {
// 每一个任务开始时, 将等待组增加1
wg.Add(1)
// 开启一个并发
go func(url string) {
// 使用defer, 表示函数完成时将等待组值减1
defer wg.Done()
// 使用http访问提供的地址
_, err := http.Get(url)
// 访问完成后, 打印地址和可能发生的错误
fmt.Println(url, err)
// 通过参数传递url地址
}(url)
}
// 等待所有的任务完成
wg.Wait()
fmt.Println("over")
}
//运行结果:
https://www.qiniu.com/
https://www.golangtc.com/
http://www.github.com/