Go语言基础二-并发编程|青训营

78 阅读5分钟
  • 并发:多线程程序在一个核的CPU上运行 并行:多线程程序在多个核上的CPU上运行

  • 协程:用户态,轻量级线程,栈KB级别 线程:内核态,线程跑多个协程,栈MB级别

  • goroutine

     //快速打印“hello goroutine”
     func hello(i int){
         println("hello goroutine:" + fmt.Sprint(i))
     }
     ​
     go func HelloGoRoutine(){
         for i:=0;i<5;i++{
             hello(j)
         }(i)
         time.Sleep(time.Second)
     }
    
  • 协程之间的通信:CSP(Communicating Sequential Processes)提倡通过通信共享内存而不是通过共享内存而实现通信

  • channel

     make(chan 元素类型,[缓冲大小])
     make(chan int)//无缓冲通道,同步通道
     make(chan int,1)//有缓冲通道
    
  • WaitGroup实现并发编程的同步

    • ADD(delta int)计数器+delta
    • Done()计数器-1
    • Wait()阻塞直到计数器为0
     var wg sync.WaitGroup
     wg.Add
     ...
    
  • 并发
    • 基本概念

      • 进程:程序在操作系统中的一次执行过程,系统进行资源分配和调度的一个独立单位。
      • 线程:操作系统基于进程开启的轻量级进程,是操作系统调度执行的最小单位
      • 协程:非操作系统提供而是由用户自行创建和控制的用户态“线程”,比线程更轻量级
    • 并发模型

      • 线程&锁模型
      • Actor模型
      • CSP模型
      • Fork&Join模型
      • Go语言中的并发程序主要是通过基于CSP的goroutine和channel来实现
    • Goroutine是Go语言支持并发的核心,在一个Go程序中同时创建成百上千个goroutine是非常普遍的,一个goroutine会以一个很小的栈开始其生命周期,一般只需要2KB

      • 区别于操作系统线程由系统内核进行调度, goroutine 是由Go运行时(runtime)负责调度。例如Go运行时会智能地将 m个goroutine 合理地分配给n个操作系统线程,实现类似m:n的调度机制,不再需要Go开发者自行在代码层面维护一个线程池。
      • Goroutine 是 Go 程序中最基本的并发执行单元。每一个 Go 程序都至少包含一个 goroutine——main goroutine,当 Go 程序启动时它会自动创建。
      • 在Go语言编程中不需要去自己写进程、线程、协程,需要让某个任务并发执行的时候,只需要把这个任务包装成一个函数,开启一个 goroutine 去执行这个函数就可以了
    • go关键字

      • 使用goroutine在函数或方法在调用前加上go关键字就可以创建一个goroutine,从而让该函数或方法在新创建的goroutine中执行
       go f()//创建一个新的goroutine运行函数f
       go func(){
       //
       }()
       //一个goroutine必定对应一个函数/方法,可以创建多个goroutine去执行相同的函数/方法
      
    • 启动单个goroutine

      • 在调用函数(普通函数和匿名函数)前加上一个go关键字

         package main
         import "fmt"
         func hello(){
             fmt.Println("hello")
         }
         func main(){
             hello()
             fmt.Println("你好")
         }
         /*
         hello
         你好
         */
         ​
        
         func main(){
             go hello()//启动另一个goroutine去执行hello()函数
             fmt.Println("你好")
         }
         /*
         你好
         */
        
      • 在Go程序启动时,会为main函数创建一个默认的goroutine。在上面的代码中在main函数中使用go关键字创建了另外一个goroutine去执行hello函数,而此时main goroutine还在继续往下执行,此时存在两个并发执行的goroutine。

      • 当main函数结束时整个程序也就结束了,同时main goroutine也结束了,所有由main goroutine创建的goroutine也会一同退出。

      • 上述”hello“没有打印出的原因是:main函数退出太快,另外一个goroutine中的函数还未执行完程序就退出了,导致未打印出“hello”

         package main
         import "fmt"
         func hello(){
             fmt.Println("hello")
         }
         func main(){
             go hello()
             fmt.Println("你好")
             time.Sleep(time.Second)//time.Sleep一秒钟
         }
         /*
         你好
         hello
         */
        
      • 先打印“你好”的原因:在程序中创建goroutine执行函数需要一定的开销,与此同时main函数所在的goroutine是继续执行的image-20230727074513362

      • Go语言中通过sync包提供了一些常用的并发原语。

         //当并不关心并发操作的结果或有其它方式收集并发操作的结果时,WaitGroup是实现等待一组并发操作完成的好方法
         packgae main
         import (
             "fmt"
             "sync"
         )
         var wg sync.WaitGroup//声明全局等待组变量
         func hello(){
             fmt.Println("hello")
             wg.Done()//告知当前goroutine完成
         }
         func main(){
             wg.Add(1)//登记一个goroutine
             go hello()
             fmt.Println("你好")
             wg.Wait()//阻塞等待登记的goroutine完成
         }
         /*
         你好
         hello
         */
         //此次程序不会有多余的停顿,hello goroutine执行完毕后程序直接退出
        
    • 启动多个goroutine

       package main
       import (
           "fmt"
           "sync"
       )
       var wg sync.WaitGroup
       func hello(i int){
           defer wg.Done()//
           fmt.Println("hello",i)
       }
       func main(){
           for i:=0;i<10;i++{
               wg.Add(1)
               go hello(i)
           }
           wg.Wait()
       }
       //每次终端上打印数字的顺序都不一致,因为10个goroutine是并发执行的,而goroutine的调度是随机的
      
    • 动态栈

      • 操作系统的线程一般都有固定的栈内存(通常为2MB),而Go语言中一个goroutine的初始栈空间一般为2KB。
      • Go的runtime会自动为goroutine分配合适的栈空间
    • goroutine调度

      • 操作系统内核调度时会挂起当前正在执行的线程并将寄存器中的内容保存到内存中,然后选出接下来要执行的线程并从内存中恢复该线程的寄存器信息,然后恢复执行该线程的现场并开始执行线程。从一个线程切换到另一个线程需要完整的上下文切换,因为可能需要多次内存访问,索引整个切换上下文的操作开销较大,会增加运行的cpu周期。
      • 区别于操作系统内核调度操作系统线程,goroutine的调度是Go语言运行时(runtime)层面的实现,是完全由Go语言本身实现的一套调度系统—go scheduler。按照一定的规则将所有的goroutine调度到操作系统线程上执行。