浅谈Go语言并发编程
一、并发和并行
-
并行是我们通常认为的那个同时做多件事情;
-
并发则是在线程这个模型下产生的概念。并发表示同时发生了多件事情,通过时间片切换,哪怕只有单一的核心,也可以实现“同时做多件事情”这个效果。
-
根据底层是否有多处理器,并发与并行是可以等效的,这并不是两个互斥的概念。
-
举个例子,我们说资源请求并发数达到了1万。这里的意思是有1万个请求同时过来了。但是这里很明显不可能真正的同时去处理这1万个请求的吧!如果这台机器的处理器有4个核心,不考虑超线程,那么我们认为同时会有4个线程在跑。也就是说,并发访问数是1万,而底层真实的并行处理的请求数是4。如果并发数小一些只有4的话,又或者你的机器牛逼有1万个核心,那并发在这里和并行一个效果。也就是说,并发可以是虚拟的同时执行,也可以是真的同时执行。而并行的意思是真的同时执行。
-
结论是:并行是我们物理时空观下的同时执行,而并发则是操作系统用线程这个模型抽象之后站在线程的视角上看到的“同时”执行。
二、为什么Go是并发编程呢,与其他编程语言相比的不同在哪呢
-
Go语言是为并发而生的语言,Go语言是为数不多的在语言层面实现并发的语言;
-
Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。
-
CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
-
Go语言打出的标语是“不要用共享内存的方式来通信,而要以通信的方式来共享内存”
-
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包”java.util.concurrent”中的数据结构。Go中也实现了传统的线程并发模型。
-
Go的CSP并发模型,是通过goroutine和channel来实现的。
-
goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
-
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
三、Go并发编程模型
Goroutine - 并发执行
- 类似于 UNIX 中的 &
- 很像线程,但更轻量,可以简单理解为线程
- 一个 goroutine 就是一个独立运行的函数
- 当一个 goroutine 阻塞时,所在的线程会阻塞,但其它 goroutine 不受影响
- 通过关键字 go 来创建 goroutine,如下:
func StartRoutine() {
do something
}
go StartRoutine()
- 所有的go函数都是并发执行的,谁先谁后并不能确定,所以在程序设计过程中不要对这种并发执行的先后顺序做任何假设。如果我们确实希望如此,就必须通过额外的手段去实现
- go函数是可以有结果声明的,但它们返回的结果值会在其执行完成后被丢弃。如果想把go函数的计算结果传递给其他go函数,将通过Channel来实现
Channel - Goroutine间的通信通道
- Channel提供了一种机制,它既可以同步两个并发执行的函数,又可以让这两个函数通过相互传递特定类型的值来通信
- Channel让我们更容易编写清晰、正确的并发程序
- Channel有点类似Java中常用来做多线程间数据通信的阻塞队列BlockingQueue
- Channel的组成是一个数据环形数组队列,用于存储消息元素 + 两个链表实现的 goroutine阻塞队列,用于存储阻塞在 recv 和 send 操作上的 goroutine + 互斥锁Mutex,用于各个属性变动的同步
- 内部实现了make(创建)、send(发送)、receive(接收)、close(关闭)四个方法管理Channel的生命周期
- 分为无缓存Channel和有缓存Channel,无缓存Channel可以理解为容量为1的阻塞队列,有缓存Channel则是容量为N(N>1)的阻塞队列
- Channel的玩法很多,根据实际场景编写合适的Go并发程序
基本操作
//创建channel
ch := make(chan int, 10)
//发送数据到channel
ch <- x
//从channel接收数据
x <- ch
//另一种方式接收数据
x = <- ch
//关闭chennel
close(configChan)
Range遍历
//创建channel
ch := make(chan int, 10)
//发送数据到channel
ch <- 1
ch <- 2
ch <- 3
//通过range遍历接收通道数据
for x := range ch {
fmt.Println(x)
}
Select监听
- select 用法类似与 IO 多路复用,可以同时监听多个 channel 的消息状态(发送和接收状态)
- 执行 select 时,若只有一个 case 通过(不阻塞),则执行这个 case 块
- 若有多个 case 通过,则随机挑选一个 case 执行
- 若所有 case 均阻塞,且定义了 default 模块,则执行 default 模块。若未定义 default 模块,则 select 语句阻塞,直到有 case 被唤醒。
- 使用 break 会跳出 select 块。
//创建channel
ch1 := make(chan int, 10)
ch2 := make(chan int, 10)
ch3 := make(chan int, 10)
//一些场景中,一些 worker goroutine 需要一直循环处理信息,直到收到 quit 信号
quit:= make(chan struct{})
//定义超时
timeout := time.After(5 * time.Second)
//通过select监听通道状态变化
select {
case <- ch1:
...
case <- ch2:
...
case ch3 <- 10:
...
case <- timeout:
...
case <- quit:
return
default:
...
}
单向通道
//只能用于发送的通道
sendCh := make(chan<- int, 10)
//只能用于接收的通道
receiveCh := make(<-chan int, 10)