开发者

详解Go语言中如何通过Goroutine实现高并发

开发者 https://www.devze.com 2024-10-14 10:58 出处:网络 作者: 景天科技苑
目录一、并发编程的基本概念二、进程、线程、协程三、Goroutine四、Goroutine的创建和使用1. 创建Goroutine2. Goroutine的规则五、Goroutine的生命周期和调度六、使用runtime包管理Goroutine七、多线程会遇到的问题1
目录
  • 一、并发编程的基本概念
  • 二、进程、线程、协程
  • 三、Goroutine
  • 四、Goroutine的创建和使用
    • 1. 创建Goroutine
    • 2. Goroutine的规则
  • 五、Goroutine的生命周期和调度
    • 六、使用runtime包管理Goroutine
      • 七、多线程会遇到的问题
        • 1. 临界资源的安全问题
        • 2. 经典案例:售票问题

      一、并发编程的基本概念

      并发编程是指在一个程序中同时运行多个任务,这些任务可以独立地执行,也可以相互协作。并发编程可以提高程序的执行效率,特别是在处理大量I/O操作或计算密集型任务时。

      在Go语言中,并发编程主要通过goroutine和channel来实现。

      Goroutine是Go语言独有的并发执行单元,它允许函数或方法并发执行,而无需手动管理线程。

      Channel是Go语言中进行goroutine间通信和同步的主要机制。

      二、进程、线程、协程

      **程序:**指令和数据的一个有序集合。本身没有任何含义,是一个静态的概念。

      进程:QQ.exe 微信 … 一个个的程序、执行起来之后,开启一个进程。执行程序的一次执行过程,它是动态的概念。进程是系统资源分配的单位。

      **线程:**一个进程中可以有多个线程,并行的,一个进程之中,至少要有一个线程。main 主线程

      • 线程是CPU调度和执行的单位。
      • 一个线程,直接执行就可以了
      • 多个线程:CPU如何调度执行。 一个CPU、也是可以跑多个线程的。

      并行和并发

      • 并发:一个http://www.devze.comcpu同一时间不停执行多个程序
      • 并行:多个cpu同一时间不停执行多个程序

      详解Go语言中如何通过Goroutine实现高并发

      在代码级别中的所谓多线程并发处理问题。模拟出来的

      • 真正的多线程是指的拥有多个CPU、多核
      • 如果是模拟出来的多线程、即在一个CPU的情况下,在同一个时间点,只能执行一个线程的代码
      • 因为执行的速度很快,所以就有了同时在执行的一种错觉。

      并行真的就最快吗?

      • 并行运行的组件,考虑到多个线程之间的通信问题,这种跨CPU通信问题是开销比较高的,并行并不一定快。

      详解Go语言中如何通过Goroutine实现高并发

      进程

      进程是一个程序在一编程客栈个数据集中的一次动态执行过程,可以简单理解为“正在执行的程序",它是CPU资源分配和调度的独立单位。

      进程一般由程序、数据集、进程控制块三部分组成。我们编写的程序用来描述进程要完成哪些功能以及如何完成;

      数据集则是程序在执行过程中所需要使用的资源;进程控制块用来记录进程的外部特征,描述进程的执行变化过程,系统可以利用它来控制和管理进程,它是系统感知进程存在的唯一标志。 进程的局限是创建、撤销和切换的开销比较大

      线程 :

      线程是在进程之后发展出来的概念。线程也叫轻量级进程,它是一个基本的CPU执行单元,也是程序执行过程中的最小单元,由线程ID、 程序计数器、寄存器集合和堆栈共同组成。一个进程可以包含多个线程。

      线程的优点是减小了程序并发执行时的开销,提高了操作系统的并发性能,**缺点是线程没有自己的系统资源,同一进程的各线程可以共享进程所拥有的系统资源,如果把进程比作一个车间,那么线程就好比是车间里面的工人。**不过对于某些独占性资源存在锁机制,处理不当可能会产生”死锁"。

      协程Goroutine

      协程是一种用户态的轻量级线程,又称微线程,英文名Coroutine,协程的调度完全由用户控制。人们通常将协程和子程序(函数)比较着理解。

      就好比是启动了一个函数,单次执行完毕它。不影响我们main线程的执行。

      子程序调用总是一个入口,一次返回,一旦退出即完成了子程序的执行。

      与传统的系统级线程和进程相比,协程的最大优势在于其"轻量级&zuUwNEfhrdquo;可以轻松创建上百万个而不会导致系统资源衰竭,而线程和进程通常最多也不能超过1万的。这也是协程也叫轻量级线程的原因。

      补充点:Go语言流行的主要一个原因,高并发的问题。高效!

      Go语言对于并发的实现是靠协程,Goroutine

      三、Goroutine

      Go中使用Goroutine来实现并发concurrently

      **Goroutine是Go语言特有的名词。**区别于进程Process,线程Thread, 协程Goroutine, 因为Go语言的创造者们觉得和他们是有所区别的,所以专门创造了Goroutine

      Goroutine是与其他函数或方法同时运行的函数或方法。Goroutines可以被认为是轻量级的线程。与线程相比,创建Goroutine的成本很小,它就是一段代码,一个函数入口。以及在堆上为其分配的一个堆栈(初始大小为4K,会随着程序的执行自动增长删除)。因此它非常廉价,Go应用程序可以轻松并发运行数千个Goroutines

      在go语言中python使用 goroutine,在调用函数或者方法前面加上 go 关键字即可

      普通方法调用 对比 多线程

      **普通方法调用:**串行

      main(){ // 串行执行 1/2/3
          test1()
          test2()
          test3()
      }
      

      多线程

      main(){ // 4个线程同时执行:main test1  test2 test3 、交替的快速执行。
         go test1()
         go test2()
         go test3()
      }
      

      详解Go语言中如何通过Goroutine实现高并发

      四、Goroutine的创建和使用

      Goroutine是由Go的运行时调度和管理的轻量级线程。每个goroutine都有自己独立的栈空间,并且由Go的运行时环境进行调度。

      与线程不同,goroutine的创建和切换成本更低,因为它只是函数入口和堆栈的封装,不需要像线程那样进行复杂的上下文切换。

      1. 创建Goroutine

      在Go语言中,使用go关键字来创建一个新的goroutine。例如:

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          //使用go关键字来创建goroutine协程
          go Hello()
          time.Sleep(1 * time.Second) // 等待一秒钟,确保Hello函数有足够的时间执行。如果不加等待,main函数如果结束了,所有的 goroutine也会瞬间销毁
      }
      
      func Hello() {
          fmt.Println("hello world")
      }
      

      详解Go语言中如何通过Goroutine实现高并发

      2. Goroutine的规则

      1、当新的Goroutine开始时, Goroutine调用立即返回。与函数不同,go不等待Goroutine执行结束

      2、当Goroutine调用,并且Goroutine的任何返回值被忽略之后,go立即执行到下一行代码

      3、main的Goroutine应该为其他的Goroutines执行。如果main的Goroutine终止了,程序将被终止,而其他Goroutine将不会运行

      五、Goroutine的生命周期和调度

      Go程序在一个主Goroutine中启动,这个主Goroutine封装了main函数。主Goroutine会进行一系列的初始化工作,包括设置每个goroutine能申请的栈空间的最大尺寸、启动垃圾回收Goroutine等。

      当使用go关键字创建一个新的Goroutine时,Go的运行时会创建一个新的轻量级线程,并分配一个Goroutine栈,然后将该Goroutine添加到调度器中。

      调度器会根据调度算法选择一个可用的Goroutine运行。如果当前没有可用的Goroutine,程序可能会进入休眠状态。

      Goroutine在调用某些会引起阻塞的函数时,会被暂停,直到函数返回结果。

      这些函数包括I/O操作、网络请求、系统调用、锁等。

      当Goroutine阻塞时,调度器会将它放回到队列中,直到它再次准备好运行。

      主Goroutine - mian

      封装main函数的goroutine称为主goroutine。

      主goroutine所做的事情并不是执行main函数那么简单。它首先要做的是:设定每一个goroutine所能申请的栈空间的最大尺寸。

      在32位的计算机系统中此最大尺寸为250MB,而在64位的计算机系统中此尺寸为1GB。

      如果有某个goroutine的栈空间尺寸大于这个限制,那么运行时系统就会引发一个栈溢出(stack overflow)的运行时恐慌。随后,这个go程序的运行也会终止。

      此后,主goroutine会 进行一系列的初始化工作,涉及的工作内容大致如下:

      1、创建一个特殊的defer语句,用于在主goroutine退出时做必要的善后处理。因为主goroutine也可能非正常的结束

      2、启动专用于在后台清扫内存垃圾的goroutine,并设置GC可用的标识.

      3、执行main包中所引用包下的init函数

      4、执行main函数

      执行完main函数后,它还会检查主goroutine是否引发了运行时恐慌,并进行必要的处理。

      程序运行完毕后,主goroutine会结束自己以及当前进程的运行。

      六、使用runtime包管理Goroutine

      Go语言的runtime包提供了与Go运行时环境交互的各种功能,包括垃圾回收、并发控制、程序退出、堆栈管理等。以下是一些常用的runtime包函数:

      • runtime.GOMAXPROCS:设置最大可运行的操作系统线程数。
      • runtime.NumCPU:返回机器的CPU核心数。
      • runtime.NumGoroutine:返回当前运行的Goroutine数量。
      • runtime.Gosched:让出CPU时间片,使得其他Goroutine可以运行。
      • runtime.Goexit:退出当前的Goroutine。
      • runtime.KeepAlive:确保某个Goroutine不会被垃圾回收。
      • runtime.SetFinalizer:为对象设置终结器,当垃圾回收器准备回收该对象时,会调用该终结器。
      • runtime.GC:强制运行垃圾回收器。
      • runtime.GOOS: 获取操作系统名称

      这些函数可以帮助我们更好地管理Goroutine和并发编程。

      例如,可以使用runtime.GOMAXPROCS来设置最大可运行的操作系统线程数,从而控制并发度。

      可以使用runtime.NumGoroutine来监控当前运行的Goroutine数量,以便进行性能调优。

      获取系统的信息runtime

      package main
      
      import (
          "fmt"
          "runtime"
      )
      
      // 获取系统的信息runtime
      func main() {
          // 获取goRoot目录 : 找到指定目录,存放一些项目信息。
          fmt.Println("GoRoot Path:", runtime.GOROOT())
          // 获取操作系统  Windows ,可以根据操作系统类型判断盘符分隔符。 “\\”  “/”
          fmt.Println("System:", runtime.GOOS)
          // 获取cpu数量 12, 可以尝试做一些系统优化,开启更大的栈空间。
          fmt.Println("Cpu num:", runtime.NumCPU())
      }
      

      详解Go语言中如何通过Goroutine实现高并发

      并发控制

      package main
      
      import (
          "fmt"
          "runtime"
      )
      
      // 控制并发顺序
      func main() {
          // goroutine是竞争cpu的  ,调度
          go func() {
              for i := 0; i < 5; i++ {
                  fmt.Println("goroutine", i)
              }
          }()
      
          for i := 0; i < 5; i++ {
              // gosched:礼让, 让出时间片,让其他的 goroutine 先执行
              // cpu是随机,相对来说,可以让一下,但是不一定能够成功
              // schedule
              runtime.Gosched()
              fmt.Println("main-", i)
          }
      }
      

      正常情况下,main主函数里面的协程一般是先于go func()执行的,但是我们在main函数里面加上了runtime.Gosched(),这样main函数里面的代码执行就让出CPU时间片,让其他Goroutine先执行

      详解Go语言中如何通过Goroutine实现高并发

      Goroutine的终止和清理

      使用runtime.Goexit()终止Goroutine

      runtime.Goexit()函数用于终止当前Goroutine的执行。当调用Goexit()时,当前Goroutine会立即停止执行,并把控制权交还给调度器。

      这意味着当前Goroutine不会继续执行后续的代码,也不会返回到调用它的地方。同时,其他仍在运行的Goroutine将继续执行。例如:

      package main
      
      import (
          "fmt"
          "runtime"
          "time"
      )
      
      func main() {
          go func() {
              defer fmt.Println("A.defer")
      
              func() {
                  defer fmt.Println("B.defer")
                              //return // 只是终止了函数
      
                  //这里设置,终止当前的 goroutine,该Goroutine中下面的代码不再执行
                  runtime.Goexit()
                  defer fmt.Println("C.defer")
                  fmt.Println("B")
              }()
      
              fmt.Println("A")
          }()
      
          time.Sleep(1 * time.Second)
      }
      

      详解Go语言中如何通过Goroutine实现高并发

      七、多线程会遇到的问题

      1. 临界资源的安全问题

      临界资源:指并发环境中多个进程、线程、协程共享的资源

      在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。

      package main
      
      import (
          "fmt"
          "time"
      )
      
      func main() {
          // 此时的a就是临界资源:多个协程共http://www.devze.com享的变量,会导致程序结果未知
          a := 1
      
          go func() {
              a = 2
              fmt.Println("goroutine a:", a)
          }()
      
          a = 3
          time.Sleep(3 * time.Second)
          fmt.Println("main a:", a)
      }
      

      详解Go语言中如何通过Goroutine实现高并发

      2. 经典案例:售票问题

      并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。

      如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的

      package main
      
      import (
          "fmt"
          "time"
      )
      
      // 定义全局变量 票库存为10张
      var ticket int = 10
      
      func main() {
          // 单线程不存在问题,多线程资源争抢就出现了问题
          //多人抢票
          go saleTickets("张三")
          go saleTickets("李四")
          go saleTickets("王五")
          go saleTickets("赵六")
      
          time.Sleep(time.Second * 5)
      }
      
      // 售票函数
      func saleTickets(name string) {
          for {
              if ticket > 0 {
                  time.Sleep(time.Millisecond * 1)
                  fmt.Println(name, "剩余票的数量为:", ticket)
                  //每卖出一张票,票的数量减一
                  ticket--
              } else {
                  fmt.Println("票已售完")
                  break
              }
          }
      }
      

      多线程都在操作数据,出现了负数这种不合理的结果

      详解Go语言中如何通过Goroutine实现高并发

      发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。

      以上就是详解Go语言中如何通过Goroutine实现高并发的详细内容,更多关于Go Goroutine实现高并发的资料请关注编程客栈(www.devze.com)其它相关文章!

      0

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      关注公众号