01 并发 vs 并行
并发(Concurrent):在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行。
并行(Parallel):当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
简单来说,并发相当于一个人同时在吃米饭、猪肉、蔬菜和水果,假设你一次只能吃一样食物(当然,现实中你可以混着一起吃),显然你只是在一段时间内吃了米饭、猪肉、蔬菜、水果,但一个时刻你只是吃了一种食物。而并行则是多个人一起吃米饭、猪肉、蔬菜和水果,在一个时刻,你可能在吃米饭,而另一个人可能在吃猪肉,从而达到在同一时刻在吃不同种类食物的效果。
02 协程 Goroutine
线程:线程是进程的一个实体,是 CPU 调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。其栈属于MB级别。
协程:协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。其栈属于KB级别。
一个线程可以多个协程。Go程序会智能地将协程中的任务合理地分配给每个CPU。Go语言之所以被称为现代化的编程语言,就是因为它在语言层面已经内置了调度和上下文切换的机制。天然的可实现高并发。
使用 Goroutine 快速打印的小例子
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 5; i++ {
go hello(i)
}
time.Sleep(time.Second)
}
func hello(i int) {
fmt.Printf("hello goroutine %d\n", i)
}
运行结果
hello goroutine 4
hello goroutine 0
hello goroutine 1
hello goroutine 2
hello goroutine 3
可以看到是乱序输出的,说明hello()是并行执行的。
03 CSP (Communicating Sequential Processes)
Go实现了两种并发形式。第一种是大家普遍认知的:多线程共享内存。其实就是Java或者C++等语言中的多线程开发。另外一种是Go语言特有的,也是Go语言推荐的:CSP(communicating sequential processes)并发模型。
CSP并发模型是在1970年左右提出的概念,属于比较新的概念,不同于传统的多线程通过共享内存来通信,CSP讲究的是“以通信的方式来共享内存”。
请记住下面这句话:
Do not communicate by sharing memory; instead, share memory by communicating.
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”
普通的线程并发模型,就是像Java、C++、或者Python,他们线程间通信都是通过共享内存的方式来进行的。非常典型的方式就是,在访问共享数据(例如数组、Map、或者某个结构体或对象)的时候,通过锁来访问,因此,在很多时候,衍生出一种方便操作的数据结构,叫做“线程安全的数据结构”。例如Java提供的包java.util.concurrent中的数据结构。Go中也实现了传统的线程并发模型。
Go的CSP并发模型,是通过goroutine和channel来实现的。
goroutine是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
生成一个goroutine的方式非常的简单:Go一下,就生成了。
go f();
通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。
在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。
而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
有两个goroutine,其中一个发起了向channel中发起了传值操作。(goroutine为矩形,channel为箭头)
左边的goroutine开始阻塞,等待有人接收。
这时候,右边的goroutine发起了接收操作。
右边的
goroutine也开始阻塞,等待别人传送。
这时候,两边goroutine都发现了对方,于是两个goroutine开始一传,一收。
这便是Golang CSP并发模型最基本的形式。
一个通过channel通信的例子
package concurrence
func CalSquare() {
src := make(chan int)
dest := make(chan int, 3)
go func() {
defer close(src)
for i := 0; i < 10; i++ {
src <- i
}
}()
go func() {
defer close(dest)
for i := range src {
dest <- i * i
}
}()
for i := range dest {
println(i)
}
}
一个通过WaitGroup来阻塞协程的例子
package main
import (
"fmt"
"sync"
)
func hello(i int) {
println("hello world : " + fmt.Sprint(i))
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1) // 计数器 + 1
go func(j int) {
defer wg.Done() // 计算器 - 1
hello(j)
}(i)
}
wg.Wait() // 进行阻塞
}