Go语言学习教程之goroutine和通道的示例详解
来源:脚本之家
时间:2022-12-22 20:33:44 240浏览 收藏
本篇文章向大家介绍《Go语言学习教程之goroutine和通道的示例详解》,主要包括语言goroutine、通道,具有一定的参考价值,需要的朋友可以参考一下。
goroutine
goroutine
是由Go运行时管理的轻量级线程。
go f(x, y, z)
在一个新的goroutine中开始执行f(x, y,z)
。
goroutines运行在相同的地址空间中,所以对共享的内存访问必须同步。sync
包提供了基本的同步原语(synchronization primitives),比如互斥锁(mutual exclusion locks)。
goroutines运行在相同的地址空间中,没有内存隔离,不同的goroutines可以访问同一个内存地址。这样对共享的内存的访问就可能出现问题,比如有一个全局变量A,goroutine 1开始修改A的数据,但是还没修改完,goroutine 2就开始读取A的数据了,这样读到的数据可能是不准确的,如果goroutine 2中也要修改A的数据,那A的数据就处于一种更不确定的状态了。所以需要使用互斥锁,当goroutine 1开始修改A的数据之前,先加个锁,表示这块内存已经被锁上了,等修改完A的数据再将锁解开。在goroutine 1修改数据A但还没修改完的期间,goroutine 2需要修改/读取A的内容,发现已经加锁,就会进入休眠状态,直到变量A的锁被解开才会执行goroutine 2中的修改/读取。
package main import ( "fmt" "time" ) func main() { go say("a") say("b") } func say(s string) { for i := 0; i执行
go run goroutine.go
的时候,会在主goroutine中执行main
函数,当执行到go say("a")
的时候,会在一个新的goroutine中执行say("a")
(称这个子goroutine为goroutine 1),然后主goroutine中继续执行say("b")
,主goroutine和goroutine 1中的函数执行是并发的。因为是并发执行,打印出的字符串a和字符串b的顺序是无法确定的。
(仔细观察的话会发现打印的前2条数据的时间戳,b的时间戳在a的后面,但是先打印出了b,这说明这次执行中,两者的fmt.Println函数的执行(直到输出到终端)时间不同,先拿到了字符串a,但是打印字符串a的fmt.Println执行比打印字符串b的函数执行稍稍慢了一点,所以b先出现在了输出界面上。可能背后还有更复杂的原因,这里不作深究。)
通道
通道(channels)是一个类型化的管道(conduit),可以通过
(通道运算符)来使用通道,对值进行发送和接收。
可选的
操作符指定了通道的方向,如果给出了一个方向,通道就是定向的,否则就是双向的。
chan T // 可以被用来发送和接收类型为T的值 chan如果有
操作符的话,数据按照箭头的方向流动。
通道在使用前必须被创建:
make(chan int, 100)通过内置的
make
函数创建一个新的、初始化的通道,接收的参数是通道类型和一个可选的容量。容量设置缓存区的大小。如果容量是0或者省略了,通道就是非缓存的,只在发送方和接收方都准备好的时候才能通信成功。否则通道就是缓存的,发送方的缓存区没有满,或者接收方的缓存区不为空,就能不阻塞地进行通信。“发送方的缓存区没有满,或者接收方的缓存区不为空,就能不阻塞地进行通信。“这句话直白一点说,就是如果缓存区满了,就不能再往通道中发送数据了(
chan ),如果缓存区是空的,就不能从通道中接收数据了(
)。
1.无缓存通道例子:
package main import ( "fmt" "sync" "time" ) var wg sync.WaitGroup func main() { example1() wg.Wait() // 等待所有goroutines执行完成 } func example1() { chan1 := make(chan int) wg.Add(1) go a(chan1) // 向通道中发送数字1、2 wg.Add(1) go b(chan1) // 等待1秒之后,从通道中拿数据,拿到的是数字2 fmt.Println("接收数据A",如果把以下这两句注释掉,运行代码就会报错:
fatal error: all goroutines are asleep - deadlock!
。wg.Add(1) go b(chan1) // 等待1秒之后,从通道中拿数据把这句注释掉,代码变成了往无缓存通道中发送了2个元素,但是只接收了1个元素。由于向通道中发送的元素2没被接收,通道会阻塞,sync包又在等待数字2的发送(
chan1 )完成,就造成了死锁。
最终在无缓存通道中的元素个数为0,无缓存通道就不会阻塞。
2.有缓存通道例子:
... var wg sync.WaitGroup func main() { example2() wg.Wait() // 等待所有goroutines执行完成 } func example2() { chan1 := make(chan int, 2) wg.Add(1) go a(chan1) // 向通道中发送数字1、2、3 fmt.Println("接收数据",以上代码向容量为2的缓存通道中发送了3个元素,但是只接收了1个,此时通道中还有2个元素,不会阻塞。
如果在
a
函数的最后一行再加上一句chan1 ,再执行代码,就会报错
fatal error: all goroutines are asleep - deadlock!
。因为发送了4个元素,只接收了1个元素,还剩3个元素没被接收,3 > 2,缓存已经满了,由于代码中没有别的地方来接收元素,通道阻塞,但是sync
包又在等待chan1 的完成,所以会造成死锁。
最终在有缓存通道中的元素个数小于等于容量,有缓存通道就不会阻塞。
3.使用通道在goroutines间进行通信的例子:
func main() { example3() } func example3() { 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 :=这段代码将数组的内容分为两部分,在两个goroutines中分别进行计算,最后再进行求和。
这里两个子goroutines是与主goroutine并发执行的,但主goroutine中的
x, y := 依然拿到了两个子goroutines中往通道发送的数据(
c )。这是因为通道的发送和接收会阻塞,直到另一边准备好。
x
拿到的是先计算完的和,y
拿到的是后计算完的和,x
,y
的值是不确定的,可能是-5 17 或者 17 -5,就看哪个子goroutine中的计算先完成。Range 和 Close
发送方可以
close
一个通道来表明没有更多的值会被发送。接收方可以通过赋值第二个参数给接收表达式,测试一个通道是否已经被关闭。执行如下语句:
v, ok :=如果没有更多的值要接收,并且通道已经关闭了,
ok
的值就为false
。
for i := range c
循环,从通道中重复地接收值,直到通道关闭。注意:
- 只有发送方可以关闭一个通道,接收方不可以。在一个已经关闭的通道上进行发送会导致一个错误(panic)。
- 通道不像文件,不需要总是关闭它们。关闭只有必须告诉接收方不会再来更多值时,才是必须的,比如终止一个
range
循环。
func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } } func fibonacci(n int, c chan int) { x, y := 0, 1 for i := 0; i以上代码求斐波那契数列,依次将求得的值发送到通道。
如果把
close(c)
语句注释掉,运行代码,就会报错:fatal error: all goroutines are asleep - deadlock!
。因为for i := range c
一直在等通道关闭,但是整个执行过程中并没有关闭通道,造成了死锁。Select
select
语句让一个goroutine等待多个通信操作。一个
select
会阻塞,直到它的cases中的一个可以运行,然后它就会执行该case。如果多个通信都准备好了,就会随机选择一个。func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i上述代码还是实现一个斐波那契数列的计算。
在子goroutine(称之为goroutine 1)中循环10次,依次从通道c中接收数据,循环结束之后,将数字0发送到通道quit。
在主goroutine中,调用fibonacci函数:
-
c 是向通道中发送数据,只要有地方从通道中接收数据,向通道中发送数据就能继续运行。每次在goroutine 1的循环中
,主goroutine中的select语句中的
case c 中的语句就会执行。
是从通道中接收数据,只要有地方向通道中发送数据,从通道中接收数据就能继续运行。当goroutine 1中循环结束之后
quit ,
case 中的语句就会执行。
一个select
中的default
case,在没有其他case准备好的时候就会运行。
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) for { select { case每隔100毫秒,通道tick就会收到一次数据,
case 中的语句会执行,打印一次
tick.
;500毫秒之后,通道boom
会收到数据,case 中的语句会执行,打印
BOOM!
,并且使用return
结束程序的执行。在这期间,由于for
语句是一直在循环的,当通道tick
和通道boom
中都没收到数据时,就会执行default
中的语句:打印一个点并且等待50毫秒。粗略看了下
time.Tick
和time.After
代码,两者返回的值都是类型为的通道,使用轮询,在满足时间条件之后,向通道中发送当前时间。如果想看通道中传递的时间数据的话,可以使用以下代码:
package main import ( "fmt" "time" ) func main() { tick := time.Tick(100 * time.Millisecond) boom := time.After(500 * time.Millisecond) var x, y time.Time for { select { case x, _ =sync.Mutex
如果我们想要避免冲突,确保一次只有一个goroutine可以访问一个变量(这个概念称为互斥),则可以使用互斥锁(mutex)。
Go的标准库提供了互斥的使用,需要用到
sync.Mutex
和它的两个方法Lock
和Unlock
。package main import ( "fmt" "sync" "time" ) func main() { c := SafeCounter{v: make(map[string]int)} for i := 0; i官方留的两道练习题
官方留了两道练习题,没有给出完整的代码。可以作为了解了以上知识之后的练手。
等价的二叉树
有很多不同的二叉树,存储着相同的值的序列。例如,下图两棵二叉树存储的序列是1, 1, 2, 3, 5, 8, 13。
1.实现
Walk
函数。2.测试
Walk
函数。函数
tree.New(k)
构造了一个随机结构(但总是排序的)的二叉树来存储值k
,2k
,3k
,...,10k
。创建一个新的通道
ch
并开始遍历:go Walk(tree.New(1), ch)然后打印树中包含的10个值,应该是数字1,2,3,...,10。
3.实现
Same
函数,使用Walk
来决定t1
和t2
是否存储相同的值。4.测试
Same
函数:
Same(tree.New(1), tree.New(1))
应该返回true
,Same(tree.New(1), tree.New(2))
应该返回false
。
代码实现
主要部分代码如下:
package main import ( "equbintrees/tree" "fmt" ) func main() { tree1 := tree.New(1) tree2 := tree.New(2) fmt.Println(Same(tree1, tree2)) } // 函数遍历树 t,将树中的所有值依次发送到通道中 func Walk(t *tree.Tree, ch chan int) { if t == nil { return } if t.Left != nil { Walk(t.Left, ch) } ch网络爬虫
使用Go的并发功能来并发网络爬虫。
修改
Crawl
函数来并发获取URLs,并且相同的URL不会获取2次。提示:你可以使用映射缓存已经获取到的URL,但是只使用映射对于并发使用来说是不安全的。
代码实现
这部分我尝试实现了下,主要思路是在递归的过程中,将遍历到链接中包含的urls发送到通道ch中,用
for urls := range ch
遍历通道中的元素,以此来等待所有发送到通道中的urls都被接收,在递归过程中判断深度是否达到4,达到4之后调用close(ch)关闭通道。但是有问题,因为不能仅凭 深度是否达到 来判断 是否关闭通道。给出的例子实际只有4层链接,如果设置深度需要到达到5,当递归到尽头的时候就应该关闭通道了,但是因为没有达到深度5,没有关闭通道,
for urls := range ch
还会继续等通道接收数据,但已经不会再往通道中发送数据了,造成死锁。总之,手动调用close(ch)
来正确关闭通道有点难,因为很难找到递归和并发请求时不会再往通道中发送数据的那个时机。我从这个链接找到了大佬的代码实现:https://rmoff.net/2020/07/03/learning-golang-some-rough-notes-s01e10-concurrency-web-crawler/
主要思路就是使用
sync.WaitGroup
,用Add
方法添加WaitGroup
计数,用wg.Wait()
等待所有的goroutines执行结束。主要部分代码如下:
func main() { wg := &sync.WaitGroup{} wg.Add(1) go Crawl("https://golang.org/", 5, fetcher, wg) wg.Wait() } type URLs struct { c map[string]bool // 用于存放表示一个链接是否被抓取过的映射 mux sync.Mutex // 使用互斥锁在并发的执行中进行安全的读写 } var u URLs = URLs{c: make(map[string]bool)} // 检查链接是否已经被抓取过 func (u URLs) IsCrawled(url string) bool { fmt.Printf("\n👀 Checking if %v has been crawled…", url) u.mux.Lock() defer u.mux.Unlock() if _, ok := u.c[url]; ok == false { fmt.Printf("…it hasn't\t") return false } fmt.Printf("…it has\t") return true } // 将链接标记为抓取过 func (u URLs) Crawled(url string) { u.mux.Lock() u.c[url] = true u.mux.Unlock() } // 递归地请求抓去url的数据,直到一个最大深度 func Crawl(url string, depth int, fetcher Fetcher, wg *sync.WaitGroup) { defer wg.Done() if depth ✅ found: %s %q\n", url, body) for _, z := range urls { wg.Add(1) go Crawl(z, depth-1, fetcher, wg) } }源码地址
https://github.com/renmo/myBlog/tree/master/2022-05-31-goroutine
理论要掌握,实操不能落!以上关于《Go语言学习教程之goroutine和通道的示例详解》的详细介绍,大家都掌握了吧!如果想要继续提升自己的能力,那么就来关注golang学习网公众号吧!
-
221 收藏
-
290 收藏
-
484 收藏
-
109 收藏
-
234 收藏
-
419 收藏
-
234 收藏
-
155 收藏
-
457 收藏
-
309 收藏
-
225 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习
-
- 务实的萝莉
- 受益颇多,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢up主分享文章!
- 2023-03-04 20:44:28
-
- 心灵美的学姐
- 写的不错,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢博主分享文章!
- 2023-02-17 23:21:16
-
- 迷路的彩虹
- 这篇技术文章出现的刚刚好,太细致了,受益颇多,码起来,关注楼主了!希望楼主能多写Golang相关的文章。
- 2023-02-10 12:37:51
-
- 怕孤独的大山
- 这篇技术文章真及时,很详细,受益颇多,码住,关注作者大大了!希望作者大大能多写Golang相关的文章。
- 2023-02-03 15:10:31
-
- 沉默的小土豆
- 这篇博文太及时了,作者加油!
- 2023-02-01 22:19:20
-
- 背后的芹菜
- 赞 👍👍,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,帮助很大,总算是懂了,感谢楼主分享文章!
- 2023-01-14 07:13:58
-
- 知性的酸奶
- 太全面了,已收藏,感谢博主的这篇技术贴,我会继续支持!
- 2023-01-03 00:44:49
-
- 贪玩的糖豆
- 这篇文章太及时了,好细啊,受益颇多,已加入收藏夹了,关注老哥了!希望老哥能多写Golang相关的文章。
- 2023-01-02 18:24:12
-
- 有魅力的帅哥
- 这篇文章内容出现的刚刚好,太详细了,很有用,已加入收藏夹了,关注作者大大了!希望作者大大能多写Golang相关的文章。
- 2022-12-28 13:05:55
-
- 阳光的心情
- 很棒,一直没懂这个问题,但其实工作中常常有遇到...不过今天到这,看完之后很有帮助,总算是懂了,感谢大佬分享技术文章!
- 2022-12-26 08:25:43