首页 > 其他分享 >go并发 - goroutine

go并发 - goroutine

时间:2023-11-19 09:45:24浏览次数:19  
标签:协程 goroutine 调度 并发 线程 func go Go

概述

Go并发模型独树一帜,简洁、高效。Go语言最小执行单位称为协程(goroutine),运行时可以创建成千万上个协程,这在Java、C等线程模型中是不可想象的,并发模型是Go的招牌能力之一。很多文章描述协程是轻量级的线程,并不准确,两者在底层有本质区别。线程是由操作系统维护,以Linux为例,系统调用创建线程,并由操作系统调度执行,在内核空间管理、与进程共享PCB对象、共享堆空间、独立调用栈和寄存器,是操作系统最小的调度对象,软中断触发操作系统切换调度。协程是由Go运行时维护,与操作系统线程不是对等关系,多个协程简共享堆栈空间,在用户空间维护,由Go运行时自行调度。不依赖系统中断可以做了非常轻量级。

调度可简单理解就是如何安排任务,合理高效的调度任务,可显著提升性能和降低复杂度。以Linux网络Io模型为例,经过多年的发展也就出现五种模型(阻塞 I/O、非阻塞 I/O、多路复用 I/O、信号驱动 I/O、异步 I/O)。传输层不变、TCP/IP协议栈不变、应用层协议不变、操作系统不变、硬件配置不变,不同Io模型性能差别非常大,这就是调度的威力。操作系统对线程的调度是自闭环的,不提供用户侧的控制接口,并行线程数与CPU数一致,线程切换是很重的操作,没有优化空间,完全寄托于操作系统进程管理能力。协程运行在线程之上,由go运行时维护,创建、同步、销毁、调度等,全部用户空间完成,可以做到和函数栈调用一样轻量级。在Go底层与操作系统交互还是线程模型,从操作系统视角根本看不到协程的存在,并行线程数也没有改变,复杂度也并没有降低,只是从用户侧转移到了Go运行时,总有人要负重前行。go并发模型并没有提升性能,更大作用是降低并发编程难度,降低开发人员心智。

Go的调度模型有专用名词:GPM

  • G,表示协程,用户通过go指令创建,数量不受限制
  • P,类似CPU,内部维护了队列,G只有加入到P队列后才能被调度,数量由Go自己维护,可通过GOMAXPROCS指定数量
  • M,OS线程抽象,负责调度任务,和某个P绑定,从P的中不断取出G,切换堆栈并执行,数量不可指定,由Go Runtime调整

image.png

基本使用

一如既往的简洁,使用go指令就可以丝滑的创建一个协程,新协程将会由go运行时调度。

func main() {
    go func() {
        fmt.Println("hello world")
    }()
}

注意,上面代码大概率无法正常工作,不能打印出字符串。匿名函数在新协程中调度执行,main函数在主协程继续执行,两者协程会并发执行。这引出了协程重要特性,go主协程有特权,当主协程执行完毕就退出程序,不管是否存在用户协程。main执行结束退出程序,此时匿名函数还没来得及打印字符串。Java主线程也有类似的特性,但是开放了daemon属性可控制,Go则没有提供控制API。

要顺利打印字出字符串,主协程需要等待用户协程执行结束,本质是协程之间协调问题。

func main() {
    go func() {
        fmt.Println("hello world")
    }()
    time.Sleep(time.Second)		// 主协程睡眠1秒
}

主协程睡眠了1秒,好像很可以工作了,但这是很low的解决方法,极度不靠谱。只要涉及并发编程,就绕不开同步机制,这是并发编程的核心内容,也是并发编程的复杂度所在,独立章节介绍。

另一个方法可以阻塞主协程,等待通知后继续执行

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        fmt.Println("hello world")
    }()

    wg.Wait()	// 进入等待
}

程序会锁死并panic崩溃退出。主线程进入了等待,却没有收到通知,go运行时可以发现死锁状态,类似逻辑在Java中不会退出,将永远阻塞,因为通知底层依赖操作系统中断机制,Java编译器无法识别死锁问题。而go在用户空间调度,由自己处理调度、同步,大部分死锁问题在编译时候就可以发现。这也可以看出两种调度模型的区别,不过JDK20也支持了虚拟线程,与go协程类似在空间实现调度。

修正后代码如下

func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)

    go func() {
        fmt.Println("hello world")
        wg.Done()	// 通知
    }()

    wg.Wait()		// 进入等待
}

使用管道也可以实现通知

func main() {
    notice := make(chan bool)
    go func() {
        fmt.Println("hello")
        notice <- true
    }()

    <-notice		// 读取时堵塞,直到读取成功
}

当然也可以使用具名函数启动协程

func working()  {
    fmt.Println("hello")
}
go working()

方法启动协程

type Person struct {
    Name string
}

func (p *Person) GetName() {
    fmt.Println(p.Name)
}

p := &Person{Name: "name"}
go p.GetName()				// 启动协程

在协程执行的函数返回值将被丢弃,无法接收。如果需要返回值,只能使用一些特殊的方法
使用管道接收

func working(resultChannel chan int) {
    ....
    resultChannel<- res;	// 将结果写入管道
}

使用指针接收,调用函数时候传入指针,把结果写入指针指向的内存,这种叫传入传出参数,在C语言中比较常见

func working(data *int) {
    ....
    *data = res				// 结果写入指针指向内存
}

与其他语言一样,Go没有提供主动中断协程的API,大多数使用chan+select实现优雅退出,需要小心处理,容易出现协程泄漏问题。另外任何协程中出现panic,整个程序会崩溃,可根据情况按需捕获

func working() {
    defer func() {
        if err := recover(); err != nil {	// 捕获错误,程序不会panic
            fmt.Println("error:", err)
        }
    }()

    ...			// 业务逻辑
}


go working()	// 启动协程

标签:协程,goroutine,调度,并发,线程,func,go,Go
From: https://www.cnblogs.com/asdfzxv/p/17841618.html

相关文章

  • docker跑mongo主从复制
    docker跑mongo主从复制这是docker-compose.ymlversion:'3.1'services:mongo:container_name:'mongo'hostname:'mongo'image:mongorestart:alwaysvolumes:-./root/mongo:/root/mongo-./root/mon......
  • break continue goto(不太懂)
     ......
  • 【Python自动化】定时自动采集,并发送微信告警通知,全流程案例讲解!
    目录一、概要二、效果演示三、代码讲解3.1爬虫采集行政处罚数据3.2存MySQL数据库3.3发送告警邮件&微信通知3.4定时机制四、总结一、概要您好!我是@马哥python说,一名10年程序猿。我原创开发了一套定时自动化爬取方案,完整开发流程如下:采集数据->筛选数据->存MySQL数据库......
  • 2023-11-18:用go语言,如果一个正方形矩阵上下对称并且左右对称,对称的意思是互为镜像, 那
    2023-11-18:用go语言,如果一个正方形矩阵上下对称并且左右对称,对称的意思是互为镜像,那么称这个正方形矩阵叫做神奇矩阵。比如:1551633663361551这个正方形矩阵就是神奇矩阵。给定一个大矩阵n*m,返回其中神奇矩阵的数目。1<=n,m<=1000。来自左程云。答案2023-11-18:go,c......
  • 2023-11-18:用go语言,如果一个正方形矩阵上下对称并且左右对称,对称的意思是互为镜像, 那
    2023-11-18:用go语言,如果一个正方形矩阵上下对称并且左右对称,对称的意思是互为镜像,那么称这个正方形矩阵叫做神奇矩阵。比如:1551633663361551这个正方形矩阵就是神奇矩阵。给定一个大矩阵n*m,返回其中神奇矩阵的数目。1<=n,m<=1000。来自左程云。答案2......
  • python数据持久化(mysql+CSV+mongodb)
    1.创建数据库createdatabasemydbcharsetutf8;usemydb;createtablemydb(namevarchar(100),starvarchar(200),timevarchar(100))charset=utf8;2.使用pymysql模块在mytab表中插入一条表记录importpymysql#(1)创建数据库连接对象db=pymysql.connect('localhost','roo......
  • go map删除元素后内存是否会释放
    go底层map是由若干个bmap(桶)构成的,桶只会扩容,不会缩容,所以map中占用的内存不会被释放但是!!!以上只针对值类型的数据结构例如:基本类型intstringslicestruct等如果key为指针变量删除后这个指针变量内存不会释放,但是这个指针指向的对象,引用计数会-1如果引用......
  • django目录介绍与运行 app新建及配置
    1django目录介绍-day60项目名 -app01app的名字 -migrations数据库变更相关记录(你不要删,也不要改)-admin.py后台管理(创建项目如果没有选,就没有)-apps.pyapp的相关配置,不用管-models.py数据库相关(重点)-tests.py测试......
  • use google server
    1logininVMloginintheconsoleandthenchangetheconfigurationsudo-ivim/etc/ssh/sshd_configchangethefollowinglinesPermitRootLoginyesPasswordAuthenticationyesrebootandthenchangeyoupassword passwd#ifyouwantchangeotheru......
  • 28_rust_无畏并发
    无畏并发Concurrent:程序不同部分之间独立执行;Parallel:程序不同部分同时运行。rust无畏并发:允许编写没有细微Bug的代码。并在不引入新Bug的情况下易于重构。这里所说的“并发”泛指concurrent和parallel。使用线程同时运行代码1:1模型:实现线程的方式:通过调用OS的API创建线程。......