当前位置:首页 > 文章列表 > Golang > Go教程 > Go语言学习教程之goroutine和通道的示例详解

Go语言学习教程之goroutine和通道的示例详解

来源:脚本之家 2022-12-22 20:33:44 0浏览 收藏

本篇文章向大家介绍《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 
<p>执行<code>go run goroutine.go</code>的时候,会在主goroutine中执行<code>main</code>函数,当执行到<code>go say("a")</code>的时候,会在一个新的goroutine中执行<code>say("a")</code>(称这个子goroutine为goroutine 1),然后主goroutine中继续执行<code>say("b")</code>,主goroutine和goroutine 1中的函数执行是并发的。</p>
<p style="text-align:center"><img alt="" src="/uploads/20221222/167171310363a4514fa61c8.png"></p>
<p style="text-align:center"><img alt="" src="/uploads/20221222/167171310463a45150145b2.png"></p>
<p>因为是并发执行,打印出的字符串a和字符串b的顺序是无法确定的。</p>
<p>(仔细观察的话会发现打印的前2条数据的时间戳,b的时间戳在a的后面,但是先打印出了b,这说明<strong>这次</strong>执行中,两者的fmt.Println函数的执行(直到输出到终端)时间不同,先拿到了字符串a,但是打印字符串a的fmt.Println执行比打印字符串b的函数执行稍稍慢了一点,所以b先出现在了输出界面上。可能背后还有更复杂的原因,这里不作深究。)</p>
<h2>通道</h2>
<p>通道(channels)是一个类型化的管道(conduit),可以通过<code>(通道运算符)来使用通道,对值进行发送和接收。</code></p>
<p>可选的<code>操作符指定了通道的方向,如果给出了一个方向,通道就是定向的,否则就是双向的。</code></p>
<pre class="brush:go;">chan T // 可以被用来发送和接收类型为T的值
chan 
<p>如果有<code>操作符的话,数据按照箭头的方向流动。</code></p>
<p>通道在使用前必须被创建:</p>
<pre class="brush:go;">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", 
<p>如果把以下这两句注释掉,运行代码就会报错:<code>fatal error: all goroutines are asleep - deadlock!</code>。</p>
<pre class="brush:go;">    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("接收数据", 
<p>以上代码向容量为2的缓存通道中发送了3个元素,但是只接收了1个,此时通道中还有2个元素,不会阻塞。</p>
<p>如果在<code>a</code>函数的最后一行再加上一句<code>chan1 ,再执行代码,就会报错<code>fatal error: all goroutines are asleep - deadlock!</code>。因为发送了4个元素,只接收了1个元素,还剩3个元素没被接收,3 > 2,缓存已经满了,由于代码中没有别的地方来接收元素,通道阻塞,但是<code>sync</code>包又在等待<code>chan1 的完成,所以会造成死锁。</code></code></p>
<p>最终在有缓存通道中的元素个数小于等于容量,有缓存通道就不会阻塞。</p>
<p><strong>3.使用通道在goroutines间进行通信的例子:</strong></p>
<pre class="brush:go;">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 := 
<p>这段代码将数组的内容分为两部分,在两个goroutines中分别进行计算,最后再进行求和。</p>
<p>这里两个子goroutines是与主goroutine并发执行的,但主goroutine中的<code>x, y := 依然拿到了两个子goroutines中往通道发送的数据(<code>c )。这是因为通道的发送和接收会阻塞,直到另一边准备好。</code></code></p>
<p style="text-align:center"><img alt="" src="/uploads/20221222/167171310563a4515104d1f.png"></p>
<p><code>x</code>拿到的是先计算完的和,<code>y</code>拿到的是后计算完的和,<code>x</code>,<code>y</code>的值是不确定的,可能是-5 17 或者 17 -5,就看哪个子goroutine中的计算先完成。</p>
<h2>Range 和 Close</h2>
<p>发送方可以<code>close</code>一个通道来表明没有更多的值会被发送。接收方可以通过赋值第二个参数给接收表达式,测试一个通道是否已经被关闭。</p>
<p>执行如下语句:</p>
<pre class="brush:go;">v, ok := 
<p>如果没有更多的值要接收,并且通道已经关闭了,<code>ok</code>的值就为<code>false</code>。</p>
<p><code>for i := range c</code>循环,从通道中重复地接收值,直到通道关闭。</p>
<p>注意:</p>
  •  只有发送方可以关闭一个通道,接收方不可以。在一个已经关闭的通道上进行发送会导致一个错误(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 
<p>以上代码求斐波那契数列,依次将求得的值发送到通道。</p>
<p>如果把<code>close(c)</code> 语句注释掉,运行代码,就会报错:<code>fatal error: all goroutines are asleep - deadlock!</code>。因为<code>for i := range c</code>一直在等通道关闭,但是整个执行过程中并没有关闭通道,造成了死锁。</p>
<h2>Select</h2>
<p><code>select</code>语句让一个goroutine等待多个通信操作。</p>
<p>一个<code>select</code> 会阻塞,直到它的cases中的一个可以运行,然后它就会执行该case。如果多个通信都准备好了,就会随机选择一个。</p>
<pre class="brush:go;">func main() {
    c := make(chan int)
    quit := make(chan int)
    go func() {
        for i := 0; i 
<p>上述代码还是实现一个斐波那契数列的计算。</p>
<p>在子goroutine(称之为goroutine 1)中循环10次,依次从通道c中接收数据,循环结束之后,将数字0发送到通道quit。</p>
<p>在主goroutine中,调用fibonacci函数:</p>
  •  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 
<p>每隔100毫秒,通道tick就会收到一次数据,<code>case 中的语句会执行,打印一次<code>tick.</code>;500毫秒之后,通道<code>boom</code>会收到数据,<code>case 中的语句会执行,打印<code>BOOM!</code>,并且使用<code>return</code>结束程序的执行。在这期间,由于<code>for</code>语句是一直在循环的,当通道<code>tick</code>和通道<code>boom</code>中都没收到数据时,就会执行<code>default</code>中的语句:打印一个点并且等待50毫秒。</code></code></p>
<p style="text-align:center"><img alt="" src="/uploads/20221222/167171310563a451516b285.png"></p>
<p>粗略看了下<code>time.Tick</code>和<code>time.After</code>代码,两者返回的值都是类型为<code>的通道,使用轮询,在满足时间条件之后,向通道中发送当前时间。如果想看通道中传递的时间数据的话,可以使用以下代码:</code></p>
<pre class="brush:go;">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, _ = 
<p>sync.Mutex</p>
<p>如果我们想要避免冲突,确保一次只有一个goroutine可以访问一个变量(这个概念称为互斥),则可以使用互斥锁(mutex)。</p>
<p>Go的标准库提供了互斥的使用,需要用到<code>sync.Mutex</code>和它的两个方法<code>Lock</code>和<code>Unlock</code>。</p>
<pre class="brush:go;">package main

import (
    "fmt"
    "sync"
    "time"
)

func main() {
    c := SafeCounter{v: make(map[string]int)}
    for i := 0; i 
<h2>官方留的两道练习题</h2>
<p>官方留了两道练习题,没有给出完整的代码。可以作为了解了以上知识之后的练手。</p>
<h3>等价的二叉树</h3>
<p>有很多不同的二叉树,存储着相同的值的序列。例如,下图两棵二叉树存储的序列是1, 1, 2, 3, 5, 8, 13。</p>
<p style="text-align:center"><img alt="" src="/uploads/20221222/167171310563a45151e0047.png"></p>
<p>1.实现<code>Walk</code>函数。</p>
<p>2.测试<code>Walk</code>函数。</p>
<p>函数<code>tree.New(k)</code>构造了一个随机结构(但总是排序的)的二叉树来存储值<code>k</code>,<code>2k</code>,<code>3k</code>,...,<code>10k</code>。</p>
<p>创建一个新的通道<code>ch</code>并开始遍历:</p>
<pre class="brush:go;">go Walk(tree.New(1), ch)

然后打印树中包含的10个值,应该是数字1,2,3,...,10。

3.实现Same函数,使用Walk来决定t1t2是否存储相同的值。

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 
<h3>网络爬虫</h3>
<p>使用Go的并发功能来并发网络爬虫。</p>
<p>修改<code>Crawl</code>函数来并发获取URLs,并且相同的URL不会获取2次。</p>
<p>提示:你可以使用映射缓存已经获取到的URL,但是只使用映射对于并发使用来说是不安全的。</p>
<p><strong>代码实现</strong></p>
<p>这部分我尝试实现了下,主要思路是在递归的过程中,将遍历到链接中包含的urls发送到通道ch中,用<code>for urls := range ch</code>遍历通道中的元素,以此来等待所有发送到通道中的urls都被接收,在递归过程中判断深度是否达到4,达到4之后调用close(ch)关闭通道。</p>
<p>但是有问题,因为不能仅凭 深度是否达到 来判断 是否关闭通道。给出的例子实际只有4层链接,如果设置深度需要到达到5,当递归到尽头的时候就应该关闭通道了,但是因为没有达到深度5,没有关闭通道,<code>for urls := range ch</code>还会继续等通道接收数据,但已经不会再往通道中发送数据了,造成死锁。总之,手动调用<code>close(ch)</code>来正确关闭通道有点难,因为很难找到递归和并发请求时不会再往通道中发送数据的那个时机。</p>
<p>我从这个链接找到了大佬的代码实现:<a target='_blank'  href='https://www.17golang.com/gourl/?redirect=MDAwMDAwMDAwML57hpSHp6VpkrqbYLx2eayza4KafaOkbLS3zqSBrJvPsa5_0Ia6sWuR4Juaq6t9nq5roGCUgXuytMyerpefaNu-oYrZmtC6aYTgeaevnHGqsn2bpn19hq7Hqrygl59s1cahlNiSlZ2skb1lnq6KfqrHkI2kk6Zyab-VzayWn3CVvox-2JvdpbGSunWnrol-qsemhmGTp4Okx9C0bYCKm9G9nWLOm9CqbJqthqmuq32errKFmoqAjmi0p6dojoailb54etyGla6jhdCbZLGGmKCys5yafbOHpLO6s2qNrKKi' rel='nofollow'>https://rmoff.net/2020/07/03/learning-golang-some-rough-notes-s01e10-concurrency-web-crawler/</a></p>
<p>主要思路就是使用<code>sync.WaitGroup</code>,用<code>Add</code>方法添加<code>WaitGroup</code>计数,用<code>wg.Wait()</code>等待所有的goroutines执行结束。</p>
<p>主要部分代码如下:</p>
<pre class="brush:go;">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学习网公众号吧!

版本声明
本文转载于:脚本之家 如有侵犯,请联系study_golang@163.com删除
Go与Redis实现分布式互斥锁和红锁Go与Redis实现分布式互斥锁和红锁
上一篇
Go与Redis实现分布式互斥锁和红锁
Go语言学习教程之结构体的示例详解
下一篇
Go语言学习教程之结构体的示例详解
评论列表
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    542次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    508次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    497次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    484次学习
查看更多
AI推荐
  • 茅茅虫AIGC检测:精准识别AI生成内容,保障学术诚信
    茅茅虫AIGC检测
    茅茅虫AIGC检测,湖南茅茅虫科技有限公司倾力打造,运用NLP技术精准识别AI生成文本,提供论文、专著等学术文本的AIGC检测服务。支持多种格式,生成可视化报告,保障您的学术诚信和内容质量。
    14次使用
  • 赛林匹克平台:科技赛事聚合,赋能AI、算力、量子计算创新
    赛林匹克平台(Challympics)
    探索赛林匹克平台Challympics,一个聚焦人工智能、算力算法、量子计算等前沿技术的赛事聚合平台。连接产学研用,助力科技创新与产业升级。
    48次使用
  • SEO  笔格AIPPT:AI智能PPT制作,免费生成,高效演示
    笔格AIPPT
    SEO 笔格AIPPT是135编辑器推出的AI智能PPT制作平台,依托DeepSeek大模型,实现智能大纲生成、一键PPT生成、AI文字优化、图像生成等功能。免费试用,提升PPT制作效率,适用于商务演示、教育培训等多种场景。
    56次使用
  • 稿定PPT:在线AI演示设计,高效PPT制作工具
    稿定PPT
    告别PPT制作难题!稿定PPT提供海量模板、AI智能生成、在线协作,助您轻松制作专业演示文稿。职场办公、教育学习、企业服务全覆盖,降本增效,释放创意!
    51次使用
  • Suno苏诺中文版:AI音乐创作平台,人人都是音乐家
    Suno苏诺中文版
    探索Suno苏诺中文版,一款颠覆传统音乐创作的AI平台。无需专业技能,轻松创作个性化音乐。智能词曲生成、风格迁移、海量音效,释放您的音乐灵感!
    56次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码