一文初探goroutine与channel

author author     2023-02-02     400

关键词:

耐心和持久胜过激烈和狂热。

哈喽大家好,我是陈明勇,本文介绍的内容是 ​​Go​​​ 并发模块的两个重要角色 → ​​goroutine​​​ 与 ​​channel​​​。如果本文对你有帮助,不妨点个赞,如果你是 ​​Go​​ 语言初学者,不妨点个关注,一起成长一起进步,如果本文有错误的地方,欢迎指出!

前言

Go 语言的 ​​CSP​​​ 并发模型的实现包含两个主要组成部分:一个是 ​​Goroutine​​​,另一个是 ​​channel​​。本文将会介绍它们的基本用法和注意事项。

Goroutine

​Goroutine​​​ 是 ​​Go​​​ 应用的基本执行单元,它是一种轻量的用户级线程,其底层是通过 ​​coroutine​​​(协程)去实现的并发。众所周知,协程是一种运行在用户态的用户线程,因此 ​​Goroutine​​​ 也是被调度于 ​​Go​​ 程序运行时。

基本用法

语法:go + 函数/方法

通过 go 关键字 + 函数/方法 可以创建一个 ​​Goroutine​​。

代码示例:

import (
"fmt"
"time"
)

func printGo()
fmt.Println("具名函数")


type G struct


func (g G) g()
fmt.Println("方法")


func main()
// 基于具名函数创建 goroutine
go printGo()
// 基于方法创建 goroutine
g := G
go g.g()
// 基于匿名函数创建 goroutine
go func()
fmt.Println("匿名函数")
()
// 基于闭包创建 goroutine
i := 0
go func()
i++
fmt.Println("闭包")
()
time.Sleep(time.Second) // 避免 main goroutine 结束后,其创建的 goroutine 来不及运行,因此在此休眠 1 秒

执行结果:

闭包
具名函数
方法
匿名函数

当多个 ​​Goroutine​​ 存在时,它们的执行顺序是不固定的。因此每次打印的结果都不相同。

由代码可知,通过 ​​go​​ 关键字,我们可以基于 具名函数 / 方法 创建 ​​goroutine​​,也可以基于 匿名函数 / 闭包 创建 ​​goroutine​​。

那么 ​​Goroutine​​​ 是如何退出的呢?正常情况下,只要 ​​Goroutine​​​ 函数执行结束,或者执行返回,意味着 ​​Goroutine​​​ 的退出。如果 ​​Goroutine​​​ 的函数或方法有返回值,在 ​​Goroutine​​ 退出时会将其忽略。

channel

​channel​​​ 在 Go 并发模型中扮演者重要的角色。它可以用于实现 ​​Goroutine​​​ 间的通信,也可以用来实现 ​​Goroutine​​ 间的同步。

channel 的基本操作

​channel​​​ 是一种复合数据类型,声明时需要指定 ​​channel​​ 里元素的类型。

声明语法:var ch chan string

通过上述代码声明一个元素类型为 ​​string​​​ 的 ​​channel​​​,其只能存放 ​​string​​​ 类型的元素。​​channel​​​ 是引用类型,必须初始化才能写入数据,通过 ​​make​​ 的方式初始化。

import (
"fmt"
)

func main()
var ch chan string
ch = make(chan string, 1)
// 打印 chan 的地址
fmt.Println(ch)
// 向 ch 发送 "Go" 数据
ch <- "Go"
// 从 ch 中接收数据
s := <-ch
fmt.Println(s) // Go

通过 ​​ch <- xxx​​​ 可以向 ​​channel​​​ 变量 ​​ch​​​ 发送数据,通过 ​​x := <- ch​​​ 可以从 ​​channel​​​ 变量 ​​ch​​ 中接收数据。

带缓冲 channel 与无缓冲 channel

如果初始化 ​​channel​​​ 时,不指定容量时,则创建的是一个无缓冲的 ​​channel​​:

ch := make(chan string)

无缓冲的 ​​channel​​​ 的发送与接收操作是同步的,在执行发送操作之后,对应 ​​Goroutine​​​ 将会阻塞,直到有另一个 ​​Goroutine​​ 去执行接收操作,反之亦然。如果将发送操作和执行操作放在同一个 Goroutine 下进行,会发生什么操作呢?看看下述代码:

import (
"fmt"
)

func main()
ch := make(chan int)
// 发送数据
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// 接收数据
n := <-ch
fmt.Println(n)

程序运行之后,会在 ​​ch <-​​​ 处得到 ​​fatal error​​​,提示所有的 ​​Goroutine​​​ 处于休眠状态,也就是死锁了。为避免这种情况,我们需要将 ​​channel​​​ 的发送操作和接收操作放到不同的 ​​Goroutine​​ 中执行。

import (
"fmt"
)

func main()
ch := make(chan int)
go func()
// 发送数据
ch <- 1
()
// 接收数据
n := <-ch
fmt.Println(n) // 1

由上述例子可以得出结论:无缓冲 ​​channel​​​ 的发送与接收操作,一定要放在两个不同的 ​​Goroutine​​​ 中进行,否则会发生 ​​deadlock​​ 形象。


如果指定容量,则创建的是一个带缓冲的 ​​channel​​:

ch := make(chan string, 5)

有缓冲的 ​​channel​​​ 与无缓冲的 ​​chennel​​​ 有所区别,执行发送操作时,只要 ​​channel​​​ 的缓冲区未满,​​Goroutine​​​ 不会挂起,直到缓冲区满时,再向 ​​channel​​​ 执行发送操作,才会导致 ​​Goroutine​​ 挂起。代码示例:

func main() 
ch := make(chan int, 1)
// 发送数据
ch <- 1

ch <- 2 // fatal error: all goroutines are asleep - deadlock!

声明 channel 的只发送类型和只接收类型

  • 既能发送又能接收的 ​​channel​

ch := make(chan int, 1)

通过上述代码获得 ​​channel​​ 变量,我们可以对它执行发送与接收的操作。

  • 只接收的 ​​channel​

ch := make(<-chan int, 1)

通过上述代码获得 ​​channel​​ 变量,我们只能对它进行接收操作。

  • 只发送的 ​​channel​

ch := make(chan<- int, 1)

通过上述代码获得 ​​channel​​ 变量,我们只能对它进行发送操作。

通常只发送 ​​channel​​​ 类型和只接收 ​​channel​​ 类型,会被用作函数的参数类型或返回值:

func send(ch chan<- int) 
ch <- 1


func recv(ch <-chan int)
<-ch

channel 的关闭

通过内置函 ​​ close(c chan<- Type)​​​,可以对 ​​channel​​ 进行关闭。

  • 在发送端关闭

在 ​​channel​​​ 关闭之后,将不能对 ​​channel​​​ 执行发送操作,否则会发生 ​​panic​​​,提示 ​​channel​​ 已关闭。


xxxxxxxxxx

 

1

func main() 

2

ch := make(chan int, 5)

3

ch <- 1

4

close(ch)

5

ch <- 2 // panic: send on closed channel

6

  • 管道 ​​channel​​​ 之后,依旧可以对 ​​channel​​​ 执行接收操作,如果存在缓冲区的情况下,将会读取缓冲区的数据,如果缓冲区为空,则获取到的值为 ​​channel​​ 对应类型的零值。

import "fmt"

func main()
ch := make(chan int, 5)
ch <- 1
close(ch)
fmt.Println(<-ch) // 1
n, ok := <-ch
fmt.Println(n) // 0
fmt.Println(ok) // false

```

  • 如果通过 for-range 遍历 ​​channel​​​ 时,中途关闭 ​​channel​​​ 则会导致 ​​for-range​​ 循环结束。

小结

本文首先介绍了 ​​Goroutine​​的创建方式以及其退出的时机是什么。

其次介绍了如何创建 ​​channel​​​ 类型变量的有缓冲与无缓冲的创建方式。需要注意的是,无缓冲的 ​​channel​​​ 发送与接收操作,需要在两个不同的 ​​Goroutine​​​ 中执行,否则会发送 ​​error​​。

接下来介绍如何定义只发送和只接收的 ​​channel​​​ 类型。通常只发送 ​​channel​​​ 类型和只接收 ​​channel​​ 类型,会被用作函数的参数类型或返回值。

最后介绍了如何关闭 ​​channel​​,以及关闭之后的一些注意事项。

goroutine与channels(代码片段)

goroutine(协程)大家都知道java中的线程Thread,golang没有提供Thread的功能,但是提供了更轻量级的goroutine(协程),协程比线程更轻,创办一个协程很简单,只需要go关键字加上要运行的函数,就可以实现了。看个简单的例子:packagemain... 查看详情

golang语言并发与并行——goroutine和channel的详细理解

http://blog.csdn.net/skh2015java/article/details/60330785http://blog.csdn.net/skh2015java/article/details/60330875http://blog.csdn.net/skh2015java/article/details/60330975 查看详情

todo:go语言goroutine和channel使用

TODO:Go语言goroutine和channel使用goroutine是Go语言中的轻量级线程实现,由Go语言运行时(runtime)管理。使用的时候在函数前面加“go”这个单词作为关键词,也是与普通函数的区别了。在函数前面加go关键字就可以创建一个新的goroutine... 查看详情

golang语言并发与并行——goroutine和channel的详细理解(代码片段)

...队列库来共享数据。以下是我入门的学习笔记。Go语言的goroutines、信道和死锁goroutineGo语言中有个概念叫做goroutine,这类似我们熟知的线程,但是更轻。以下的程序,我们串行地 查看详情

一文带你进行go语言工程实践(代码片段)

文章目录并发和Goroutine并发和并行的区别线程与协程的区别Goroutine用法并发的通信Channel并发安全依赖管理GOPATHGOPATH弊端GoVendorGoVendor弊端GoModule(最终解决方案依赖管理三要素配置文件版本规则杂项中心仓库管理依赖库依赖的... 查看详情

[转帖]go的goroutine以及channel的简介.

进程,线程的概念在操作系统的书上已经有详细的介绍。进程是内存资源管理和cpu调度的执行单元。为了有效利用多核处理器的优势,将进程进一步细分,允许一个进程里存在多个线程,这多个线程还是共享同一片内存空间,但... 查看详情

golang并发编程之channel(代码片段)

一、概念channel是golang语言级别提供的协程(goroutine)之间的通信方式。goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。那么goroutine之间如何进行数据的通信呢,Go提供了一个很好的通信机制channel。channel可以与Unixs... 查看详情

一文看懂go语言协程的设计与原理(代码片段)

...理背景Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:mainGoroutine。Go程序从main包 查看详情

一文看懂go语言协程的设计与原理(代码片段)

...理背景Go语言最大的特色就是从语言层面支持并发(Goroutine),Goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个Goroutine:mainGoroutine。Go程序从main包 查看详情

go语言8-goroutine和channel(代码片段)

GoroutineGo语言从语言层面上就支持了并发,这与其他语言大不一样。Go语言中有个概念叫做goroutine,这类似我们熟知的线程,但是更轻。进程、线程、协程进程和线程进程是程序在操作系统中的一次执行过程,系统进行资源分配... 查看详情

go教程(十三)goroutine和channel(代码片段)

...任务最快的方法.一些大的任务可以拆解成若干个小任务.goroutine可以让程序同时处理几个不同的任务.goroutine使用channel来协调它们的工作.channel允许goroutine互相发送数据并同步.这样一个goroutine就不会领先于另一个goroutine.它允许我... 查看详情

golangchannel用法与实现原理(代码片段)

...考文献1.简介Golangchannel是一种并发原语,用于在不同goroutine之间进行通信和同步。本质上,channel是一种类型安全的FIFO队列,它可以实现多个goroutine之间的同步和通信。channel是一种引用类型,即使是在不同的gorouti... 查看详情

go基础--goroutine和channel

goroutine在go语言中,每一个并发的执行单元叫做一个goroutine这里说到并发,所以先解释一下并发和并行的概念:并发:逻辑上具备同时处理多个任务的能力并行:物理上在同一时刻执行多个并发任务当一个程序启动时,其主函数... 查看详情

go入门:浅谈channel

...管道""输出管道"所用到的<-->经常分不清楚。channel是goroutine之间通信的一种方式,可以类比成Unix中的进程的通信方式管道。channel提供了一种通信机制,通过它,一个goroutine可以想另一goroutine发送消息。channel本身还需关联了... 查看详情

使用channel实现goroutine(代码片段)

使用channel实现goroutinepackagemainimport("fmt""time")varmessage=make(chanstring)//往channel中输入信息funcsample1()message<-"hellogorotine."//消费channel中的信息funcsample2()str:=<-messagestr=str+"runfast,runtheworld."message<-strfuncmain()gosample1()gosample2()time.Sleep(ti... 查看详情

go语言并发(代码片段)

...多个任务并行:同一时刻执行多个任务Go语言的并发通过goroutine实现。goroutine类似于线程,属于用户态的线程,我们可以根据需要创建成千上万个goroutine并发工作。goroutine是由Go语言的运行时调度完成,而线程是由操作系统调度... 查看详情

golang使用goroutines和channel异步获取url(代码片段)

查看详情

go_channel

通道可以被认为是Goroutines通信的管道。类似于管道中的水从一端到另一端的流动,数据可以从一端发送到另一端,通过通道接收。在前面讲Go语言的并发时候,我们就说过,当多个Goroutine想实现共享数据的时候,虽然也提供了传... 查看详情