Go的CSP并发模型,是通过goroutine和channel来实现的。
“不要以共享内存的方式来通信,相反,要通过通信来共享内存。”
goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
channel是Go语言中各个并发结构体(goroutine)之前的通信机制。 通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。
生成一个goroutine的方式非常的简单:Go一下,就生成了。
go f();
通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。
在通信过程中,传数据channel <- data和取数据<-channel必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。
而且不管传还是取,必阻塞,直到另外的goroutine传或者取为止。
发送,接受,传递代码如下:
package main
import "fmt"
// channel
func main() {
// 定义一个ch1变量
// 是一个channel类型
// 这个channel内部传递的数据是int类型
var ch1 chan int
var ch2 chan string
// channel是引用类型
fmt.Println("ch1:", ch1)
fmt.Println("ch2:", ch2)
// make函数初始化(分配内存):slice map channel
ch3 := make(chan int, 10)
// 通道的操作:发送、 接收、关闭
// 发送和接收都用一个符号: <-
ch3 <- 10 // 把10发送到ch3中
// ch3 <- 20
// <-ch3 // 从ch3中接收值,直接丢弃
ret := <-ch3 // 从ch3中接收值,保存到变量ret中
fmt.Println(ret)
ch3 <- 9
ch3 <- 8
ch3 <- 7
// 关闭
close(ch3)
// 1. 关闭的通道再接收,能取到对应类型的零值
ret2 := <-ch3
fmt.Println(ret2)
// 2. 往关闭的通道中发送值 会引发panic
// ch3 <- 20
// 3. 关闭一个已经关闭的通道会引发panic
// close(ch3)
}
无缓冲的通道:4*100交接棒,又称为同步通道
有缓冲的通道:可以让程序实现异步操作
package main
import "fmt"
// 无缓冲通道和缓冲通道
func recv(ch chan bool) {
ret := <-ch // 阻塞
fmt.Println(ret)
}
func main() {
ch := make(chan bool, 1)
ch <- false
// len:获取数据量,cap:获取容量
fmt.Println(len(ch), cap(ch))
go recv(ch)
ch <- true
fmt.Println("main函数结束")
}
方法一: ok判断:
// 利用for循环去通道ch1中接收值
for {
ret, ok := <-ch1 // 使用 value, ok := <-ch1 取值方式,当通道关闭的时候 ok = false
if !ok {
break
}
fmt.Println(ret)
}
方法二: for range 循环:
// 利用for range 循环去通道ch1中接收值
for ret := range ch1 {
fmt.Println(ret)
}
math/rand
// 给rand加随机数种子实现每一次执行都能产生真正的随机数
rand.Seed(time.Now().UnixNano())
ret1 := rand.Int63() // int64正整数
fmt.Println(ret1)
ret2 := rand.Intn(101) // [1, 101)
fmt.Println(ret2)
使用goroutine和channel实现一个简易的生产者消费者模型
生产者:产生随机数 math/rand
消费者:计算每个随机数的每个位的数字的和
1个生产者 20个消费者
package main
import (
"fmt"
"math/rand"
"sync"
)
var itemChan chan *item
var resultChan chan *result
var wg sync.WaitGroup
type item struct {
id int64
num int64
}
type result struct {
item *item
sum int64
}
// 生产者
func producer(ch chan *item) {
// 1. 生成随机数
var id int64
for i := 0; i < 10000; i++ {
id++
number := rand.Int63() // int64正整数
tmp := &item{
id: id,
num: number,
}
// 2. 把随机数发送到通道中
ch <- tmp
}
close(ch)
}
// 计算一个数字每个位的和
func calc(num int64) int64 {
// 123%10=12...3 sum = 0 + 3
// 12%10=1...2
// 1%10=0...1
var sum int64 // 0
for num > 0 {
sum = sum + num%10 // sum = 5 + 1
num = num / 10 // num = 0
}
return sum
}
// 消费者
func consumer(ch chan *item, resultChan chan *result) {
defer wg.Done()
for tmp := range ch {
// (*tmp).num // item.num
sum := calc(tmp.num)
// 构造result结构体
retObj := &result{
item: tmp,
sum: sum,
}
resultChan <- retObj
} // 结构体指针 *item
}
func startWorker(n int, ch chan *item, resultChan chan *result) {
for i := 0; i < n; i++ {
go consumer(ch, resultChan)
}
}
// 打印结果
func printResult(resultChan chan *result) {
for ret := range resultChan {
fmt.Printf("id:%v, num:%v, sum:%v\n", ret.item.id, ret.item.num, ret.sum)
// time.Sleep(time.Second)
}
}
func main() {
itemChan = make(chan *item, 10000)
resultChan = make(chan *result, 10000)
go producer(itemChan)
wg.Add(20)
startWorker(20, itemChan, resultChan)
// // 打印结果
wg.Wait() // 等到所有的生产result的goroutine都结束 再打印
close(resultChan)
printResult(resultChan)
// 给rand加随机数种子实现每一次执行都能产生真正的随机数
// rand.Seed(time.Now().UnixNano())
// ret1 := rand.Int63() // int64正整数
// fmt.Println(ret1)
// ret2 := rand.Intn(101) // [1, 101)
// fmt.Println(ret2)
}
语法:
select {
case ch1 <- 1:
...
case <- ch1:
...
case <- ch2:
...
}
示例:
package main
import (
"fmt"
"math"
"time"
)
// select多路复用
var ch1 = make(chan string, 100)
var ch2 = make(chan string, 100)
func f1(ch chan string) {
for i := 0; i < math.MaxInt64; i++ {
ch <- fmt.Sprintf("f1:%d", i)
time.Sleep(time.Millisecond * 50)
}
}
func f2(ch chan string) {
for i := 0; i < math.MaxInt64; i++ {
ch <- fmt.Sprintf("f2:%d", i)
time.Sleep(time.Millisecond * 100)
}
}
func main() {
go f1(ch1) // 往ch1这个通道中放f1开头的字符串
go f2(ch2) // 往ch2这个通道中放f2开头的字符串
for {
select {
case ret := <-ch1:
fmt.Println(ret)
case ret := <-ch2:
fmt.Println(ret)
default:
fmt.Println("暂时取不到值")
time.Sleep(time.Millisecond * 500)
}
}
}
// chan *item :既能接收也能发送的一个队列
// chan<- *item :只能发送的一个队列(单向通道)
// <-chan *item :只能接收的一个队列(单向通道)
我们先从线程讲起,无论语言层面何种并发模型,到了操作系统层面,一定是以线程的形态存在的。而操作系统根据资源访问权限的不同,体系架构可分为用户空间和内核空间;内核空间主要操作访问CPU资源、I/O资源、内存资源等硬件资源,为上层应用程序提供最基本的基础资源,用户空间呢就是上层应用程序的固定活动空间,用户空间不可以直接访问资源,必须通过“系统调用”、“库函数”或“Shell脚本”来调用内核空间提供的资源。
线程模型的实现,可以分为以下几种方式:
用户级线程模型
如图所示,多个用户态的线程对应着一个内核线程,程序线程的创建、终止、切换或者同步等线程工作必须自身来完成。它可以做快速的上下文切换。缺点是不能有效利用多核CPU。
内核级线程模型
这种模型直接调用操作系统的内核线程,所有线程的创建、终止、切换、同步等操作,都由内核来完成。一个用户态的线程对应一个系统线程,它可以利用多核机制,但上下文切换需要消耗额外的资源。C++就是这种。
两级线程模型
这种模型是介于用户级线程模型和内核级线程模型之间的一种线程模型。这种模型的实现非常复杂,和内核级线程模型类似,一个进程中可以对应多个内核级线程,但是进程中的线程不和内核线程一一对应;这种线程模型会先创建多个内核级线程,然后用自身的用户级线程去对应创建的多个内核级线程,自身的用户级线程需要本身程序去调度,内核级的线程交给操作系统内核去调度。
M个用户线程对应N个系统线程,缺点增加了调度器的实现难度。
Go语言的线程模型就是一种特殊的两级线程模型(GPM调度模型)。
M指的是Machine,一个M直接关联了一个内核线程。由操作系统管理。
P指的是”processor”,代表了M所需的上下文环境,也是处理用户级代码逻辑的处理器。它负责衔接M和G的调度上下文,将等待执行的G与M对接。
G指的是Goroutine,其实本质上也是一种轻量级的线程。包括了调用栈,重要的调度信息,例如channel等。
P的数量由环境变量中的GOMAXPROCS决定,通常来说它是和核心数对应,例如在4Core的服务器上回启动4个线程。G会有很多个,每个P会将Goroutine从一个就绪的队列中做Pop操作,为了减小锁的竞争,通常情况下每个P会负责一个队列。
三者关系如下图所示:
以上这个图讲的是两个线程(内核线程)的情况。一个M会对应一个内核线程,一个M也会连接一个上下文P,一个上下文P相当于一个“处理器”,一个上下文连接一个或者多个Goroutine。为了运行goroutine,线程必须保存上下文。
上下文P(Processor)的数量在启动时设置为GOMAXPROCS环境变量的值或通过运行时函数GOMAXPROCS()。通常情况下,在程序执行期间不会更改。上下文数量固定意味着只有固定数量的线程在任何时候运行Go代码。我们可以使用它来调整Go进程到个人计算机的调用,例如4核PC在4个线程上运行Go代码。
图中P正在执行的Goroutine为蓝色的;处于待执行状态的Goroutine为灰色的,灰色的Goroutine形成了一个队列runqueues。
Go语言里,启动一个goroutine很容易:go function 就行,所以每有一个go语句被执行,runqueue队列就在其末尾加入一个goroutine,一旦上下文运行goroutine直到调度点,它会从其runqueue中弹出goroutine,设置堆栈和指令指针并开始运行goroutine。
你可能会想,为什么一定需要一个上下文,我们能不能直接除去上下文,让Goroutine的runqueues挂到M上呢?答案是不行,需要上下文的目的,是让我们可以直接放开其他线程,当遇到内核线程阻塞的时候。
一个很简单的例子就是系统调用sysall,一个线程肯定不能同时执行代码和系统调用被阻塞,这个时候,此线程M需要放弃当前的上下文环境P,以便可以让其他的Goroutine被调度执行。
如上图左图所示,M0中的G0执行了syscall,然后就创建了一个M1(也有可能来自线程缓存),(转向右图)然后M0丢弃了P,等待syscall的返回值,M1接受了P,将·继续执行Goroutine队列中的其他Goroutine。
当系统调用syscall结束后,M0会“偷”一个上下文,如果不成功,M0就把它的Gouroutine G0放到一个全局的runqueue中,将自己置于线程缓存中并进入休眠状态。全局runqueue是各个P在运行完自己的本地的Goroutine runqueue后用来拉取新goroutine的地方。P也会周期性的检查这个全局runqueue上的goroutine,否则,全局runqueue上的goroutines可能得不到执行而饿死。
按照以上的说法,上下文P会定期的检查全局的goroutine 队列中的goroutine,以便自己在消费掉自身Goroutine队列的时候有事可做。假如全局goroutine队列中的goroutine也没了呢?就从其他运行的中的P的runqueue里偷。
每个P中的Goroutine不同导致他们运行的效率和时间也不同,在一个有很多P和M的环境中,不能让一个P跑完自身的Goroutine就没事可做了,因为或许其他的P有很长的goroutine队列要跑,得需要均衡。
该如何解决呢?
Go的做法倒也直接,从其他P中偷一半!
在 Go语言程序运行时(runtime)实现了一个小型的任务调度器。这套调度器的工作原理类似于操作系统调度线程,Go 程序调度器可以高效地将 CPU 资源分配给每一个任务。传统逻辑中,开发者需要维护线程池中线程与 CPU 核心数量的对应关系。同样的,Go 地中也可以通过 runtime.GOMAXPROCS() 函数做到,格式为:
runtime.GOMAXPROCS(逻辑CPU数量)
一般情况下,可以使用 runtime.NumCPU() 查询 CPU 数量,并使用 runtime.GOMAXPROCS() 函数进行设置,例如:
runtime.GOMAXPROCS(runtime.NumCPU())
Go语言的sync包提供了常见的并发编程控制锁;
在并发编程中锁的主要作用是保证多个线程或者 goroutine在访问同一片内存时不会出现混乱;
sync.Mutex可能是sync包中使用最广泛的原语。它允许在共享资源上互斥访问(不能同时访问):
mutex := &sync.Mutex{}
mutex.Lock()
// Update共享变量 (比如切片,结构体指针等)
mutex.Unlock()
必须指出的是,在第一次被使用后,不能再对sync.Mutex进行复制。(sync包的所有原语都一样)。如果结构体具有同步原语字段,则必须通过指针传递它。
sync.RWMutex是一个读写互斥锁,它提供了我们上面的刚刚看到的sync.Mutex的Lock和UnLock方法(因为这两个结构都实现了sync.Locker接口)。但是,它还允许使用RLock和RUnlock方法进行并发读取:
mutex := &sync.RWMutex{}
mutex.Lock()
// Update 共享变量
mutex.Unlock()
mutex.RLock()
// Read 共享变量
mutex.RUnlock()
sync.RWMutex允许至少一个读锁或一个写锁存在,而sync.Mutex允许一个读锁或一个写锁存在。
sync.Map是一个并发版本的Go语言的map,我们可以:
package main
import (
"fmt"
"strconv"
"sync"
"time"
)
var m = sync.Map{
}
func tickDemo() {
ticker := time.Tick(time.Second) //定义一个1秒间隔的定时器
for i := range ticker {
fmt.Println(i) //每秒都会执行的任务
// time.Sleep(time.Second*10)
}
}
func main() {
wg := sync.WaitGroup{
}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
m.Store(key, n)
value, _ := m.Load(key)
fmt.Printf("k=:%v,v:=%v\n", key, value)
wg.Done()
}(i)
}
wg.Wait()
sync.Once是一个简单而强大的原语,可确保一个函数仅执行一次。在下面的示例中,只有一个goroutine会显示输出消息:
once := &sync.Once{
}
for i := 0; i < 4; i++ {
i := i
go func() {
once.Do(func() {
fmt.Printf("first %d\n", i)
})
}()
}
Go语言中除了可以使用通道(channel)和互斥锁进行两个并发程序间的同步外,还可以使用等待组进行多个任务的同步,等待组可以保证在并发环境中完成指定数量的任务
在 sync.WaitGroup(等待组)类型中,每个 sync.WaitGroup 值在内部维护着一个计数,此计数的初始默认值为零。
等待组有下面几个方法可用,如下表所示。
方法名 | 功能 |
---|---|
(wg * WaitGroup) Add(delta int) | 等待组的计数器 +1 |
(wg * WaitGroup) Done() | 等待组的计数器 -1 |
(wg * WaitGroup) Wait() | 当等待组计数器不等于 0 时阻塞直到变 0。 |
对于一个可寻址的 sync.WaitGroup 值 wg:
package main
import (
"fmt"
"sync"
)
// 启动goroutine
// 利用sync.WaitGroup 实现优雅的等待
var wg sync.WaitGroup // 是一个结构体,它里面有一个计数器
func hello(i int) {
defer wg.Done() // 计数器-1
fmt.Println("Hello 沙河!", i)
// if i == 8 {
// panic("报错啦")
// }
}
func main() {
defer fmt.Println("哈哈哈")
wg.Add(10) // 计数器+10
for i := 0; i < 10; i++ {
go hello(i) // 1. 创建一个goroutine 2. 在新的goroutine中执行hello函数
}
fmt.Println("Hello main func.")
// time.Sleep(time.Second)
// 等hello执行完(执行hello函数的那个goroutine执行完)
wg.Wait() // 阻塞,一直等待所有的goroutine结束
fmt.Println("main函数结束")
}