GO语言的进阶之路 | 青训营笔记

133 阅读4分钟

GO语言的进阶之路

语言进阶

进程和线程

  • 进程:进程是程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。

  • 线程:线程是进程的一个执行实体,是CPU调度分派的基本单位,它是比进程更小的能独立运行的基本单位。

  • 一个进程可以创建和撤销多个线程;同一个进程中的多个线程之间可以并发执行。

  • 协程:独立的栈空间,共享堆空间,调度由用户自己控制,本质上有点类似于用户级线程,这些用户级线程的调度也是自己实现的。

  • 一个线程上可以跑多个协程,协程是轻量级的线程。

并发 VS 并行

并发:多线程程序在一个核的cpu上运行,就是并发。

并行:多线程程序在多个核的cpu上运行,就是并行。

image.png

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 即:通信顺序进程,它的核心观点是:不要以共享内存的方式来通信,而是要通过通信来共享内存

普通并发模型

image.png

CSP 并发模型

image.png

如果说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时,表示所有并发任务已经完成。