golang入门系列之浅谈 goroutine

  • Post author:
  • Post category:golang


在这里插入图片描述



写在前面


本文内容是我继

上篇

入门学习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并发模型,其实在开头的图片就能体现,是通过

goroutine



channel

来实现的。

  • 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 main

import (
	"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 <- i
		fmt.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系列的博客,可能会帮助到正在疯狂找资源的小伙伴。



版权声明:本文为qq_43390235原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。