GO语言的进阶之路
语言进阶
进程和线程
-
进程:进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
-
线程:线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。
-
一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。
-
协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。
-
一个线程上可以跑多个协程,协程是轻量级的线程。
并发 VS 并行
并发:多线程程序在一个核的cpu上运行,就是并发。
并行:多线程程序在多个核的cpu上运行,就是并行。
goroutine
通俗的来说就是我们程序员只需要定义很多个任务,然后让系统去帮助我们把这些任务分配给cpu去实现并发执行
goroutine 奉行通过通信来共享内存,而不是共享内存来通信。
使用时只需要将我们的任务放在函数里,然后在函数前面加上 go 关键字就可以了,一个goroutine 必定对应一个函数,可以创建多个goroutine执行相同的函数
func hello(){
fmt.Println("Hello goroutine!")
}
func main(){
go hello()
fmt.Println("main goroutine!")
}
如图,结果只打印了 main goroutine! 。为啥没有打印Hello goroutine!呢?
在程序启动时,Go程序就会为main()函数创建一个默认的goroutine。当main函数返回的时候该goroutine也就结束了,所有在main函数中的goroutine会一同结束。而解决这种问题最简单粗暴的方法就是让main函数等一等,所以用到了time.Sleep。
func hello(){
fmt.Println("Hello goroutine!")
}
func main(){
go hello() // 启动另外一个 goroutine 去执行hello函数
fmt.Println("main goroutine!")
time.Sleep(time.Second)
}
这一次先打印main goroutine done!,然后紧接着打印Hello Goroutine!。
首先为什么会先打印main goroutine done!是因为我们在创建新的goroutine的时候需要花费一些 时间,而此时main函数所在的goroutine是继续执行的。
Channel
GO语言的并发模型就是csp(Communicating Sequential Processes)
CSP是上个世纪七十年代提出的一种强大的并发编程模型,它的全称是 Communicating Sequential Process 即:通信顺序进程,它的核心观点是:不要以共享内存的方式来通信,而是要通过通信来共享内存
普通并发模型
CSP 并发模型
如果说goroutine是Go程序并发的执行体,channel就是它们之间的连接。channel是可以让一个 goroutine发送特定值到另一个goroutine的通信机制。
Go 语言中的通道(channel)是一种特殊的类型。通道像一个传送带或者队列,总是遵循先入先出 (First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。
channel分为有缓冲通道和无缓冲通道,无缓冲通道必须要有接收方才能实现发送
下面来举个使用channel的例子:
package main
import "fmt"
func main (){
chan1:= make( chan int , 5) // 创建有缓冲通道
chan2:= make( chan int , 5)
go func() {
for i:=0;i<5;i++{
chan1 <- i
}
close(chan1) // 关闭通道
}()
go func() {
for i := range chan1{
chan2 <- i*i
}
close(chan2)
}()
for i:= range chan2{
fmt.Println(i)
}
}
单向通道
chan<- int是一个只能发送的通道,可以发送但是不能接收;
<-chan int是一个只能接收的通道,可以接收但是不能发送。
并发安全 Lock
有时候在Go代码中可能会存在多个goroutine同时操作一个资源(临界区),这种情况会发生竞态问 题(数据竞态)。类比现实生活中的例子有十字路口被各个方向的的汽车竞争;还有火车上的卫生间被 车厢里的人竞争。
有互斥锁和读写锁等
package main
import (
"sync"
"time"
)
var (
x int
lock sync.Mutex
)
func addWithLock(){
for i:=0;i<2000;i++{
// 进入临界区
lock.Lock()
x += 1
// 退出临界区
lock.Unlock()
}
}
func addWithoutLock(){
for i:=0;i<2000;i++{
x += 1
}
}
func main (){
x = 0
for i:=0;i<10;i++{
go addWithoutLock()
}
time.Sleep(time.Second)
println("withoutLock",x)
x = 0
for i:=0;i<10;i++{
go addWithLock()
}
time.Sleep(time.Second)
println("withLock",x)
}
WaitGroup
(wg WaitGroup) Add(delta int) 计数器+delta
(wg WaitGroup) Done() 计数器-1
(wg *WaitGroup) Wait() 阻塞直到计数器变为0
sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发 任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来 等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。