当前位置: 代码迷 >> 综合 >> Go入坑系列之浅谈 goroutine
  详细解决方案

Go入坑系列之浅谈 goroutine

热度:42   发布时间:2024-02-25 13:15:13.0

在这里插入图片描述

写在前面

       本文内容是我继上篇入门学习go的个人理解,如有错误,欢迎斧正。


      之前早有耳闻 Go 在高并发场景下,能够充分利用多核,在语言层面支持并发、轻量高效等优点,奈何沉迷Java生态,未曾尝试跳出来了解。前天花了几个小时入门,冲着goroutine我入坑了(当然并不是无脑吹,协程是解决问题的思维方式,并不能解决所有的问题),所以秉承着或奉献或分享或记录的理念写一些对转语言或者入坑go的小伙伴有益的文章。


一、goroutine是什么?


1. 理解协程

协程并不是新概念,早在上世纪九十年代,在解决编译器设计问题上提出的一种协同工作思想:控制流的主动让出和恢复
通俗讲就是,要干AB两件事情,分别独立维护自身的运行状态。参与者让出控制流时,记住自身状态,在控制流返回时能从上次让出的位置恢复执行,在代码层面控制让A和B两个步骤能非阻塞来回精准切换。

2. 协程和进程、线程的区别

2.1 进程

  • 进程就是拥有时间片,活着的程序,是操作系统进行资源分配和调度的基本单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。
  • 进程由内存空间(代码,数据,进程空间,打开的文件)和一个或多个线程组成。

2.2线程

  • CPU调度和分派的基本单位。一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。
  • 一个标准的线程由线程ID,当前指令指针PC,寄存器和堆栈组成。
    在这里插入图片描述


2.3 区别(协程与上面二者的区别)


  • 协程是编译器级的,进程和线程是操作系统级的。协程不是被操作系统内核所管理的,而是完全由程序所控制,也就是在用户态执行。这样带来的好处是性能大幅度的提升,因为不会像线程切换那样消耗资源。

  • 进程和线程是操作系统级的,通过一定的API暴露给用户使用。通过调度算法,保存当前的上下文,然后从上次暂停的地方再次开始计算,重新开始的地方不可预期,每次CPU计算的指令数量和代码跑过的CPU时间是相关的,跑到os分配的cpu时间到达后就会被os强制挂起。

  • 协程不是进程也不是线程,而是一个特殊的机制,保证可以在某个地方挂起,并且可以重新在挂起处外继续运行。所以说,协程与进程、线程相比并不是一个维度的概念。协程的实现,通常是对某个语言做相应的提议,然后通过后成编译器标准,然后编译器厂商来实现该机制。协程是编译器的魔术,通过插入相关的代码使得代码段能够实现分段式的执行,重新开始的地方是yield关键字指定的,一次一定会跑到一个yield对应的地方。



    在这里插入图片描述

2.4 上下文切换

在这里插入图片描述

  • 进程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户是无感知的。进程的切换内容包括页全局目录、内核栈、硬件上下文,切换内容保存在内存中。进程切换过程是由“用户态到内核态到用户态”的方式,切换效率低。

  • 线程的切换者是操作系统,切换时机是根据操作系统自己的切换策略,用户无感知。线程的切换内容包括内核栈和硬件上下文。线程切换内容保存在内核栈中。线程切换过程是由“用户态到内核态到用户态”,切换效率中等。

  • 协程的切换者是用户(编程者或应用程序),切换时机是用户自己的程序所决定的。协程的切换内容是硬件上下文,切换内存保存在用户自己的变量(用户栈或堆)中。协程的切换过程只有用户态,即没有陷入内核态,因此切换效率高。

2.5 Go协程

有了前面铺垫,goroutine就是Go语言层面实现的协程。

Goroutine和其他语言的协程(coroutine)在使用方式上类似,但从字面意义上来看不同(一个是Goroutine,一个是coroutine),再就是协程是一种协作任务控制机制,在最简单的意义上,协程不是并发的,而Goroutine支持并发的。因此Goroutine可以理解为一种Go语言的协程。同时它可以运行在一个或多个线程上。

二、goroutine如何使用?

这里标题本来要写如何实现,但是刚接触go两天,相关知识食而不化,无法倾囊相授,后续分享。

1.Go并发模型

在这里插入图片描述
先来看一下后端最喜(tou)欢(teng)的并发吧!

程序员最关心并发量和发量。

Go实现了两种并发形式。

  1. 第一种就是大家普遍知道的Java玩的这一套,多线程共享内存。
  2. 第二种也是我刚知道的Go特有的,CSP(communicating sequential processes)并发模型。

相对于Java等通过共享内存来实现通信,在访问共享数据例如数组、Map、或者某个对象的时候,通过锁来线程安全。而Go采用的是另一种思维:


“我们不使用共享内存的方式来通信,我们通过通信来共享内存。”


在这里插入图片描述
Go的CSP并发模型,其实在开头的图片就能体现,是通过goroutinechannel来实现的。

  • goroutine 是Go语言中并发的执行单位。有点抽象,其实就是和传统概念上的”线程“类似,可以理解为”线程“。
  • channel是Go语言中各个并发结构体(goroutine)之前的通信机制。
    通俗的讲,就是各个goroutine之间通信的”管道“,有点类似于Linux中的管道。

2.goroutine和channel使用

生成一个goroutine的方式非常的简单:go 函数 就创建了一个go协程

go f();

通信机制channel也很方便,传数据用channel <- data,取数据用<-channel。
这块上一篇文章有说到。

值得注意的是在通信过程中,传数据和取数据必然会成对出现,因为这边传,那边取,两个goroutine之间才会实现通信。

而且不管传还是取,一定会阻塞,直到另外的goroutine传或者取为止。

三、实战-用go实现经典生产者消费者模型

  • 创建了两个生产数据管道,一个int类型,一个string类型;
  • 一个消费数据管道,并且定时五秒钟取数据
package mainimport ("fmt""strconv""time"
)func main() {
    ch1 := make(chan int)ch2 := make(chan string)go producer1(ch1)go producer2(ch2)go consumer(ch1, ch2)time.Sleep(time.Duration(time.Second * 10))}
func producer1(ch chan int) {
    for i := 0; ; i++ {
    ch <- ifmt.Printf("put %v to ch1\n", i)time.Sleep(time.Duration(time.Second))}
}
func producer2(ch chan string) {
    for i := 0; ; i++ {
    ch <- strconv.Itoa(i)fmt.Printf("put %v to ch2\n", i)time.Sleep(time.Duration(time.Second))}
}
func consumer(ch1 chan int, ch2 chan string) {
    tick := time.Tick(time.Duration(time.Second * 5))for {
    select {
    case v := <-ch1:fmt.Printf("get %v from chan2\n", v)case v := <-ch2:fmt.Println("get "+v + " from chan2")case <-tick:fmt.Print("wait...")}}
}

总结

本文简单的介绍了协程的概念,与进程线程的区别以及go的协程使用,goroutine的实现因为刚学两天还不完善没敢怎么写,留在后面吧!学习是一个不断吞吐消化的过程,而且输入到脑海中的知识对于自己是可以快速存取的,但是输出来让别人去阅读则需要不断地打磨加工,所以我很感谢之前学习Java时白嫖的视频中,直播课中,博客中遇到的每一位老师。

所以开一个go系列的博客,可能会帮助到正在疯狂找资源的小伙伴。