首页 > 其他分享 >Go 并发之goroutine和Channel讲解

Go 并发之goroutine和Channel讲解

时间:2024-12-29 09:00:41浏览次数:3  
标签:1.3 goroutine select Go main Channel 通道

目录

1 并发

1.1 简介

Go 语言支持并发,通过 goroutineschannels 提供了一种简洁且高效的方式来实现并发。

1.2 Goroutine

1.2.1 简介

goroutine 协程,是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的,Go 中的并发执行单位,类似于轻量级线程
Goroutine 的调度由 Go 运行时管理,用户无需手动分配线程,使用 go 关键字启动 Goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
Goroutine非阻塞的,可以高效地运行成千上万个 Goroutine
goroutine 语法格式:go 函数名( 参数列表 )
例如:go f(x, y, z)

注意

  • 当一个新的Go协程启动时,协程的调用立即返回。与函数不同,程序流程不会等待Go协程结束再继续执行。程序流程在开启Go协程后立即返回并开始执行下一行代码,并忽略Go协程的任何返回值。
  • 在主协程存在时才能运行其他协程,主协程终止则程序终止,其他协程也将终止

1.2.2 特点

goroutine特点:

  • 可增长的栈
    OS线程(操作系统线程)一般都有固定的栈内存(2MB),一个goroutine的栈在生命周期开始时只有很小的栈(2KB),goroutine的栈是不固定的,可以按需增加或者缩小,goroutine的栈大小限制可以达到1GB,虽然这种情况不多见,所以一次可以创建十万左右的goroutine是没问题的。
  • goroutine 调度
    OS线程由OS内核来调度,goroutine则是由Go运行时(runtime)自己的调度器来调度,这个调度器使用一个m:n调度的技术(复用/调度m个goroutinen个OS线程),goroutine的调度不需要切换内核语境,所以调用一个goroutine比调用个线程的成本要低很多。
  • GOMAXPROCS
    Go运行时的调度使用GOMAXPROCS参数来确定需要使用多少个OS线程来同时执行Go代码,默认值是机器上的CPU核心数。
    例如:在一个8核心的机器上,调度器会把Go代码同时调度到8个OS线程上(GOMAXPROCSm:n调度中的n)。
    Go可以通过runtime.GOMAXPROCS()函数设置当前程序并发时占用的CPU逻辑核心数。(Go1.5版本前默认是单核心执行,Go1.5版本后默认使用全部逻辑核心数)

1.2.3 检测数据访问冲突

使用 go run -race 检测数据访问冲突
比如检测 某一个内存被写的时候刚好也被另一个协程读

o run -race goroutine.go
==================
WARNING: DATA RACE
Read at 0x00c000138000 by main goroutine:
  main.main()
      /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:18 +0xfb

Previous write at 0x00c000138000 by goroutine 7:
  main.main.func1()
      /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:13 +0x68

Goroutine 7 (running) created at:
  main.main()
      /Users/xiao_xiaoxiao/go/src/learn2/goroutine/goroutine.go:11 +0xc3
==================
[11755340 5733960 10421739 12104071 7264193 14611763 1966171 6718099 11141482 2270786]
Found 1 data race(s)
exit status 66

1.2.4 示例

package main

import (
        "fmt"
        "time"
)

func sayHello() {
        for i := 0; i < 5; i++ {
                fmt.Println("Hello")
                time.Sleep(100 * time.Millisecond)
        }
}

func main() {
	// 使用 1 个逻辑核心数跑 Go 程序
    runtime.GOMAXPROCS(1)
    go sayHello() // 启动 Goroutine
    for i := 0; i < 5; i++ {
            fmt.Println("Main")
            time.Sleep(100 * time.Millisecond)
    }
}

1.3 通道(Channel)

1.3.1 普通通道

1.3.1.1 简介

通道(Channel)是用于 Goroutine 之间的数据传递,通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行通讯,避免了显式的锁机制。
go语言的并发模型是SCP,提倡通过通信共享内存而不是通过共享内存而实现通信
通道(channel)是引用类型,是一种特殊的类型通道像一个传送带或者队列,遵循先进先出原则,类似于队列,保证收发数据的顺序每一个通道都是一个具体类型的导管,在声明channel的时候需要为其指定元素类型

注意:对信道发送和接收数据默认是阻塞的

当数据发送到信道时,程序在发送语句处阻塞,直到其他协程从该信道中读取数据。类似地,当从信道读取数据时,程序在读取语句处被阻塞,直到其他协程向信道写入数据。
信道的这种特性使得协程间通信变得高效,而不是向其他编程语言一样,显式的使用锁和条件变量来达到此目的。

1.3.1.2 声明通道

声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:ch := make(chan int)

  • 如果使用 channel 之前没有 make,会出现 deadlock 错误
    声明一个 channel 类型的变量时,实际上它是一个 nil 值。未初始化的 channelnil,无法进行发送或接收操作。
  • 写数据之前,没有其他协程阻塞接收并且没有缓冲可以存,会发生 dealock
  • channel make 之后没有传入数据,提取数据的时候会 deadlock
  • channel 满了继续传入元素会 deadlock

使用 make 函数创建一个 channel,使用 chan 关键字表示 channel,通过 <- 操作符发送接收数据,如果未指定方向,则为双向通道(即:可读可写)

ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据 并把值赋给 v

go func(c chan int) { channel c } (a)//双向通道,读写均可的
go func(c <- chan int) { } (a) //只读的Channel
go func(c chan <- int) {   } (a)//只写的Channel
  • channel 传入数据, CHAN <- DATA CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据。
  • channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里。

注意:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

1.3.1.3 普通通道示例

package main

import "fmt"

func sum(s []int, c chan int) {
    sum := 0
    for _, v := range s {
        sum += v
    }
    c <- sum // 把 sum 发送到通道 c
}

func main() {
    s := []int{7, 2, 8, -9, 4, 0}

    c := make(chan int)
    go sum(s[:len(s)/2], c)
    go sum(s[len(s)/2:], c)
    x, y := <-c, <-c // 从通道 c 中接收

    fmt.Println(x, y, x+y)
}
输出结果为:

-5 17 12

1.3.2 带缓冲区通道

1.3.2.1 简介

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据,还是遵循先进先出原则

不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

  • 读取操作: 当读取一个通道时,只有通道中有值时,读取才会成功,否则会阻塞。
  • 写入操作: 往通道写入值时,只有通道能够接收(缓冲区未满或有接收方)时,写入才会成功,否则会阻塞。

1.3.2.2 带缓冲区通道示例

package main

import "fmt"

func main() {
    // 这里我们定义了一个可以存储整数类型的带缓冲通道
    // 缓冲区大小为2
    ch := make(chan int, 2)

    // 因为 ch 是带缓冲的通道,我们可以同时发送两个数据
    // 而不用立刻需要去同步读取数据
    ch <- 1
    ch <- 2

    // 获取这两个数据
    fmt.Println(<-ch)
    fmt.Println(<-ch)
}
执行输出结果为:

1
2

1.3.3 遍历

1.3.3.1 for 遍历

使用 v, ok := <-ch格式,如果通道接收不到数据后 ok 就为 false,相当于一直循环着等待有结果输出

package main

import "fmt"

var ch1 chan int

func send(ch chan int) {        
    for i:=0;i<10;i++ {        
        ch <- i
    }
    close(ch)
}

func main() {        
    ch1 = make(chan int,100)
    go send(ch1)
    // 从通道中取值
    for {   
        // 产生两个值,获取的值给 ret,是否取完的 bool 值给 ok
        ret,ok := <-ch1
        // 判断值是否取完
        if !ok {   
     	// !ok 等于 !ok == true
            break
        }
        fmt.Println(ret)
    }
}

1.3.3.2 range 遍历与关闭通道

通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入

package main

import (
    "fmt"
)

func fibonacci(n int, c chan int) {
    x, y := 0, 1
    for i := 0; i < n; i++ {
        c <- x
        x, y = y, x+y
    }
    close(c)
}

func main() {
    c := make(chan int, 10)
    go fibonacci(cap(c), c)
    // range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
    // 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据之后就结束了。
    //如果上面的 c 通道不关闭,那么 range 函数就不会结束,从而在接收第 11 个数据的时候就阻塞了。
    for i := range c {
        fmt.Println(i)
    }
}

1.3.3.3 Select

select 是 Go 中的一个控制结构,类似于 switch 语句。但是,select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行,其他不会执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
select 语句使得一个 goroutine 可以等待多个通信操作。select 会阻塞,直到其中的某个 case 可以继续执行

Go 编程语言中 select 语句的语法如下:

select {
  case <- channel1:
    // 执行的代码
  case value := <- channel2:
    // 执行的代码
  case channel3 <- value:
    // 执行的代码
    
    // 可以定义任意数量的 case
  default:
    // 所有通道都没有准备好,执行的代码
}

示例一:

package main

import (
    "fmt"
    "time"
)

func main() {

    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()
    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("received", msg1)
        case msg2 := <-c2:
            fmt.Println("received", msg2)
        }
    }
}
结果为:
received one
received two

示例二:

package main

import "fmt"

func fibonacci(c, quit chan int) {
    x, y := 0, 1
    for {
        select {
	        case c <- x:
	            x, y = y, x+y
	        case <-quit:
	            fmt.Println("quit")
	            return
        }
    }
}
func main() {
    c := make(chan int)
    quit := make(chan int)

    go func() {
        for i := 0; i < 10; i++ {
            fmt.Println(<-c)
        }
        quit <- 0
    }()
    fibonacci(c, quit)
}

示例三:

package main

import "fmt"
func main() {        
    var ch = make(chan int,1)
    for i:=0;i<10;i++ {   
        // 解决死锁问题
        select {        
            case ch <- i:	//尝试放入值,只能放入一个值,如果有值,则无法再放入
            case ret := <-ch:	//尝试取值,没有值拿就会出现死锁
            	fmt.Println(ret)
        }
    }
}
/*
0
2
4
6
8
*/

1.3.3.4 空select

package main

func main() {          
    select {   
     }
}

我们知道select语句将会被阻塞直到其中一个case分支可执行。这个例子,select语句没有任何case分支,因此它将被永久阻塞导致死锁。程序将会触发 panic,输出如下:

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [select (no cases)]:  
main.main()  
    /tmp/main.go:4 +0x20

标签:1.3,goroutine,select,Go,main,Channel,通道
From: https://www.cnblogs.com/jingzh/p/18638385

相关文章

  • Go基础之指针和反射讲解
    目录1指针1.1简介1.2使用指针1.3指针优化输出1.3.1优化输出复杂类型1.3.2去掉优化1.3.3基本类型1.4指针数组1.4.1指针数组优化1.5指向指针的指针1.6向函数传递指针参数2反射2.1reflect2.1.1示例2.2获取变量值ValueOf2.3修改变量值Value.Set2.3.1Elem方法2.3.2......
  • Go IO之文件处理,TCP&UDP讲解
    目录1文件处理1.1打开和关闭文件1.2读取文件1.2.1简单示例1.2.2中文乱码1.2.2.1bufio1.2.2.2ioutil1.3写入文件1.3.1Write和WriteString1.3.2fmt.Fprintln1.3.2.1写入文件1.3.2.2写入标准输出1.3.3bufio.NewWriter1.3.4ioutil.WriteFile2TCP&UDP2.1TCP2.1.1服......
  • Go 并发之WaitGroup,并发锁,Context
    目录1Go并发1.1WaitGroup1.2并发锁1.2.1互斥锁1.2.2读写互斥锁1.2.3sync.Once1.2.4sync.Map1.3Context1.3.1简介1.3.2主要功能1.3.3使用示例1.3.3.1取消信号1.3.3.2设置超时1.3.3.3传递值1Go并发1.1WaitGroupsync.WaitGroup是Go标准库提供的一种同步原语,常......
  • client-go InClusterConfig方法
    InClusterConfig方法packagemainimport( "context" "test/signals" "time" "os" core_v1"k8s.io/api/core/v1" metav1"k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes......
  • 2024-12-28:求出出现两次数字的 XOR 值。用go语言,给定一个数组 nums,其中的数字出现的频
    2024-12-28:求出出现两次数字的XOR值。用go语言,给定一个数组nums,其中的数字出现的频率要么是一次,要么是两次。请找出所有出现两次的数字,并计算它们的按位XOR值。如果没有数字出现两次,则返回0。1<=nums.length<=50。1<=nums[i]<=50。nums中每个数字要么出现过一......
  • 介绍一下logos这个词法分析工具,它和nom相比如何?我看lalrpop官网给出的示例就是logos配
    UUUUUUUUUUUUUUUUUUUUUULogos简介Logos是一个用于词法分析的高效Rust库,其设计目标是简单、快速且高效。它通过声明式的方式定义词法规则,并利用Rust的强类型系统生成轻量级的词法分析器。Logos的特点声明式规则:使用Rust的枚举定义每种Token类型,并通过属性宏指定......
  • C# 和 Go 的协同开发:打造高效并发与企业级应用的最佳实践
    在现代软件开发中,微服务架构和分布式系统成为主流。开发者面临着多种挑战,其中最常见的两个需求是高并发处理和复杂的企业级业务逻辑。C#和Go作为两种广泛使用的编程语言,各自有独特的优势,在应对这些挑战时能够发挥不同的作用。C#强调企业级开发的完整性和稳定性,特别适合构......
  • 从高并发到企业级应用:C# 和 Go 的完美结合
    在现代软件开发中,随着微服务架构和分布式系统的广泛应用,开发者需要应对各种高并发、高性能的需求。而在选择编程语言时,C#和Go是两种非常流行且各具优势的语言,分别擅长不同的应用场景。C#,以其强大的企业级开发支持和丰富的生态系统在后端、桌面和Web开发中占据重要地位;而......
  • 基于python+Django+mysql校园二手书籍交易平台系统设计与实现
     博主介绍:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;专注大学生毕业设计教育、辅导。所有项目都配有从入门到精通的基础知识视频课程,学习后应对毕业设计答辩,提供核心代码讲解,答辩指导。项目配有对应开发......
  • 每天40分玩转Django:在线课程平台实战
    在线课程平台实战一、系统功能概述表模块功能技术要点课程管理课程CRUD、章节管理、视频上传DjangoModels、DRF、阿里云OSS用户系统注册登录、学习记录、购买记录DjangoAuth、Session支付系统订单生成、支付宝支付、微信支付支付接口集成、异步通知视频播放在线播放、进......