前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >GoLang协程与通道---中

GoLang协程与通道---中

作者头像
大忽悠爱学习
发布于 2022-08-23 02:21:07
发布于 2022-08-23 02:21:07
83900
代码可运行
举报
文章被收录于专栏:c++与qt学习c++与qt学习
运行总次数:0
代码可运行

GoLang协程与通道---中


协程的同步:关闭通道-测试阻塞的通道

通道可以被显式的关闭;尽管它们和文件不同:不必每次都关闭。只有在当需要告诉接收者不会再提供新的值的时候,才需要关闭通道。只有发送者需要关闭通道,接收者永远不会需要。

继续看示例 goroutine2.go:我们如何在通道的 sendData() 完成的时候发送一个信号,getData() 又如何检测到通道是否关闭或阻塞?

第一个可以通过函数 close(ch) 来完成:这个将通道标记为无法通过发送操作 <- 接受更多的值;给已经关闭的通道发送或者再次关闭都会导致运行时的 panic。在创建一个通道后使用 defer 语句是个不错的办法(类似这种情况):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ch := make(chan float64)
defer close(ch)

第二个问题可以使用逗号,ok 操作符:用来检测通道是否被关闭。

如何来检测可以收到没有被阻塞(或者通道没有被关闭)?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
v, ok := <-ch   // ok is true if v received value

通常和 if 语句一起使用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
if v, ok := <-ch; ok {
  process(v)
}

或者在 for 循环中接收的时候,当关闭或者阻塞的时候使用 break:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
v, ok := <-ch
if !ok {
  break
}
process(v)

在示例程序中使用这些可以改进为版本 goroutine3.go,输出相同。

实现非阻塞通道的读取,需要使用 select。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main
import "fmt"
func main() {
    ch := make(chan string)
    go sendData(ch)
    getData(ch)
}
func sendData(ch chan string) {
    ch <- "Washington"
    ch <- "Tripoli"
    ch <- "London"
    ch <- "Beijing"
    ch <- "Tokio"
    close(ch)
}
func getData(ch chan string) {
    for {
        input, open := <-ch
        if !open {
            break
        }
        fmt.Printf("%s ", input)
    }
}

改变了以下代码:

现在只有 sendData() 是协程,getData() 和 main() 在同一个线程中:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
go sendData(ch)
getData(ch)

在 sendData() 函数的最后,关闭了通道:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func sendData(ch chan string) {
    ch <- "Washington"
    ch <- "Tripoli"
    ch <- "London"
    ch <- "Beijing"
    ch <- "Tokio"
    close(ch)
}

在 for 循环的 getData() 中,在每次接收通道的数据之前都使用 if !open 来检测:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for {
        input, open := <-ch
        if !open {
            break
        }
        fmt.Printf("%s ", input)
    }

使用 for-range 语句来读取通道是更好的办法,因为这会自动检测通道是否关闭:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for input := range ch {
      process(input)
}

关于通道的关闭的小结

  • 只有接收方goroutine所有的数据都发送完毕后才会关闭
  • 通道是种类型,是可以被垃圾回收机制回收的;通道的关闭不是必须的
  • 对一个关闭的通道再发送值就会导致panic
  • 对一个关闭的通道进行接收会一直获取值直到通道为空。
  • 对一个关闭的并且没有值的通道执行接收操作,会得到对应类型的零值。
  • 关闭一个已经关闭的通道会导致panic。

阻塞和生产者-消费者模式:

在通道迭代器中,两个协程经常是一个阻塞另外一个。如果程序工作在多核心的机器上,大部分时间只用到了一个处理器。可以通过使用带缓冲(缓冲空间大于 0)的通道来改善。比如,缓冲大小为 100,迭代器在阻塞之前,至少可以从容器获得 100 个元素。如果消费者协程在独立的内核运行,就有可能让协程不会出现阻塞。

由于容器中元素的数量通常是已知的,需要让通道有足够的容量放置所有的元素。这样,迭代器就不会阻塞(尽管消费者协程仍然可能阻塞)。然而,这实际上加倍了迭代容器所需要的内存使用量,所以通道的容量需要限制一下最大值。记录运行时间和性能测试可以帮助你找到最小的缓存容量带来最好的性能。


使用 select 切换协程

从不同的并发执行的协程中获取值可以通过关键字select来完成,它和switch控制语句非常相似也被称作通信开关;它的行为像是“你准备好了吗”的轮询机制;select监听进入通道的数据,也可以是用通道发送值的时候。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select {
case u:= <- ch1:
        ...
case v:= <- ch2:
        ...
        ...
default: // no value ready to be received
        ...
}

default 语句是可选的;fallthrough 行为,是不允许的。在任何一个 case 中执行 break 或者 returnselect 就结束了。

select 做的就是:选择处理列出的多个通信情况中的一个。

  • 如果都阻塞了,会等待直到其中一个可以处理
  • 如果多个可以处理,随机选择一个
  • 如果没有通道操作可以处理并且写了 default 语句,它就会执行:default 永远是可运行的(这就是准备好了,可以执行)。

select 中使用发送操作并且有 default 可以确保发送不被阻塞!如果没有 defaultselect 就会一直阻塞。

select 语句实现了一种监听模式,通常用在(无限)循环中;在某种情况下,通过 break 语句使循环退出。

实例演示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main
import (
    "fmt"
    "time"
)
func main() {
    ch1 := make(chan int)
    ch2 := make(chan int)
    go pump1(ch1)
    go pump2(ch2)
    go suck(ch1, ch2)
    time.Sleep(1e9)
}
func pump1(ch chan int) {
    for i := 0; ; i++ {
        ch <- i * 2
    }
}
func pump2(ch chan int) {
    for i := 0; ; i++ {
        ch <- i + 5
    }
}
func suck(ch1, ch2 chan int) {
    for {
        select {
        case v := <-ch1:
            fmt.Printf("Received on channel 1: %d\n", v)
        case v := <-ch2:
            fmt.Printf("Received on channel 2: %d\n", v)
        }
    }
}

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Received on channel 2: 5
Received on channel 2: 6
Received on channel 1: 0
Received on channel 2: 7
Received on channel 2: 8
Received on channel 2: 9
Received on channel 2: 10
Received on channel 1: 2
Received on channel 2: 11
...
Received on channel 2: 47404
Received on channel 1: 94346
Received on channel 1: 94348

注意:

channel的零值是nil。也许会让你觉得比较奇怪,nil的channel有时候也是有一些用处的。因为对一个nil的channel发送和接收操作会永远阻塞,在select语句中操作nil的channel永远都不会被select到。


通道、超时和计时器(Ticker)

time 包中有一些有趣的功能可以和通道组合使用。

其中就包含了 time.Ticker 结构体,这个对象以指定的时间间隔重复的向通道 C 发送时间值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
type Ticker struct {
    C <-chan Time // the channel on which the ticks are delivered.
    // contains filtered or unexported fields
    ...
}

间间隔的单位是 ns(纳秒,int64),在工厂函数 time.NewTicker 中以 Duration 类型的参数传入:func NewTicker(dur) *Ticker

在协程周期性的执行一些事情(打印状态日志,输出,计算等等)的时候非常有用。

调用 Stop() 使计时器停止,在 defer 语句中使用。这些都很好的适应 select 语句:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ticker := time.NewTicker(updateInterval)
defer ticker.Stop()
...
select {
case u:= <-ch1:
    ...
case v:= <-ch2:
    ...
case <-ticker.C:
    logState(status) // call some logging function logState
default: // no value ready to be received
    ...
}

time.Tick() 函数声明为 Tick(d Duration) <-chan Time,当你想返回一个通道而不必关闭它的时候这个函数非常有用:它以 d 为周期给返回的通道发送时间,d是纳秒数。如果需要像下边的代码一样,限制处理频率(函数 client.Call() 是一个 RPC 调用,这里暂不赘述:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import "time"
rate_per_sec := 10
var dur Duration = 1e9 / rate_per_sec
chRate := time.Tick(dur) // a tick every 1/10th of a second
for req := range requests {
    <- chRate // rate limit our Service.Method RPC calls
    go client.Call("Service.Method", req, ...)
}

这样只会按照指定频率处理请求:chRate 阻塞了更高的频率。每秒处理的频率可以根据机器负载(和/或)资源的情况而增加或减少。


定时器(Timer)结构体看上去和计时器(Ticker)结构体的确很像(构造为 NewTimer(d Duration)),但是它只发送一次时间,在 Dration d 之后。

还有 time.After(d) 函数,声明如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func After(d Duration) <-chan Time

在 Duration d 之后,当前时间被发到返回的通道;所以它和 NewTimer(d).C 是等价的;它类似 Tick(),但是 After() 只发送一次时间。下边有个很具体的示例,很好的阐明了 select 中 default 的作用:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main
import (
    "fmt"
    "time"
)
func main() {
    tick := time.Tick(1e8)
    boom := time.After(5e8)
    for {
        select {
        case <-tick:
            fmt.Println("tick.")
        case <-boom:
            fmt.Println("BOOM!")
            return
        default:
            fmt.Println("    .")
            time.Sleep(5e7)
        }
    }
}

输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
    .
    .
tick.
BOOM!

习惯用法:简单超时模式

要从通道 ch 中接收数据,但是最多等待1秒。先创建一个信号通道,然后启动一个 lambda 协程,协程在给通道发送数据之前是休眠的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
timeout := make(chan bool, 1)
go func() {
        time.Sleep(1e9) // one second
        timeout <- true
}()

然后使用 select 语句接收 ch 或者 timeout 的数据:如果 ch 在 1 秒内没有收到数据,就选择到了 time 分支并放弃了 ch 的读取。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
select {
    case <-ch:
        // a read from ch has occured
    case <-timeout:
        // the read from ch has timed out
        break
}

第二种形式:取消耗时很长的同步调用

也可以使用 time.After() 函数替换 timeout-channel。可以在 select 中通过 time.After() 发送的超时信号来停止协程的执行。以下代码,在 timeoutNs 纳秒后执行 select 的 timeout 分支后,执行client.Call 的协程也随之结束,不会给通道 ch 返回值:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ch := make(chan error, 1)
go func() { ch <- client.Call("Service.Method", args, &reply) } ()
select {
case resp := <-ch
    // use resp and reply
case <-time.After(timeoutNs):
    // call timed out
    break
}

注意缓冲大小设置为 1 是必要的,可以避免协程死锁以及确保超时的通道可以被垃圾回收。此外,需要注意在有多个 case 符合条件时, select 对 case 的选择是伪随机的,如果上面的代码稍作修改如下,则 select 语句可能不会在定时器超时信号到来时立刻选中 time.After(timeoutNs) 对应的 case,因此协程可能不会严格按照定时器设置的时间结束。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ch := make(chan int, 1)
go func() { for { ch <- 1 } } ()
L:
for {
    select {
    case <-ch:
        // do something
    case <-time.After(timeoutNs):
        // call timed out
        break L
    }
}

第三种形式:假设程序从多个复制的数据库同时读取。只需要一个答案,需要接收首先到达的答案,Query 函数获取数据库的连接切片并请求。并行请求每一个数据库并返回收到的第一个响应:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Query(conns []Conn, query string) Result {
    ch := make(chan Result, 1)
    for _, conn := range conns {
        go func(c Conn) {
            select {
            case ch <- c.DoQuery(query):
            default:
            }
        }(conn)
    }
    return <- ch
}

再次声明,结果通道 ch 必须是带缓冲的:以保证第一个发送进来的数据有地方可以存放,确保放入的首个数据总会成功,所以第一个到达的值会被获取而与执行的顺序无关。正在执行的协程可以总是可以使用 runtime.Goexit() 来停止。

在应用中缓存数据:

应用程序中用到了来自数据库(或者常见的数据存储)的数据时,经常会把数据缓存到内存中,因为从数据库中获取数据的操作代价很高;如果数据库中的值不发生变化就没有问题。但是如果值有变化,我们需要一个机制来周期性的从数据库重新读取这些值:缓存的值就不可用(过期)了,而且我们也不希望用户看到陈旧的数据。


协程和恢复(recover)

一个用到 recover 的程序停掉了服务器内部一个失败的协程而不影响其他协程的工作。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func server(workChan <-chan *Work) {
    for work := range workChan {
        go safelyDo(work)   // start the goroutine for that work
    }
}
func safelyDo(work *Work) {
    defer func() {
        if err := recover(); err != nil {
            log.Printf("Work failed with %s in %v", err, work)
        }
    }()
    do(work)
}

上边的代码,如果 do(work) 发生 panic,错误会被记录且协程会退出并释放,而其他协程不受影响。

因为 recover 总是返回 nil,除非直接在 defer 修饰的函数中调用,defer 修饰的代码可以调用那些自身可以使用 panic 和 recover 避免失败的库例程(库函数)。举例,safelyDo() 中 defer 修饰的函数可能在调用 recover 之前就调用了一个 logging 函数,panicking 状态不会影响 logging 代码的运行。因为加入了恢复模式,函数 do(以及它调用的任何东西)可以通过调用 panic 来摆脱不好的情况。但是恢复是在 panicking 的协程内部的:不能被另外一个协程恢复。


本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-08-17,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
go语言学习-并发编程
1.After函数:起到定时器的作用,指定的纳秒后会向返回的channel中放入一个当前时间(time.Time)的实例。
solate
2019/07/22
6220
Golang异步编程方式和技巧
Golang基于多线程、协程实现,与生俱来适合异步编程,当我们遇到那种需要批量处理且耗时的操作时,传统的线性执行就显得吃力,这时就会想到异步并行处理。下面介绍一些异步编程方式和技巧。
用户2132290
2024/04/12
1.2K0
Golang异步编程方式和技巧
Golang中的管道(channel) 、goroutine与channel实现并发、单向管道、select多路复用以及goroutine panic处理
管道(channel)是 Go 语言中实现并发的一种方式,它可以在多个 goroutine 之间进行通信和数据交换。管道可以看做是一个队列,通过它可以进行先进先出的数据传输,支持并发的读和写。
周小末天天开心
2023/10/16
7040
Golang中的管道(channel) 、goroutine与channel实现并发、单向管道、select多路复用以及goroutine panic处理
channel
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。
Michel_Rolle
2023/11/30
2.6K0
Golang之旅23-通道channel
在goroutine并发执行的时候,需要在函数和函数之间进行通信。Go语言并发模式CSP(communicating Sequents Processes),通过通信共享内存。
皮大大
2021/03/02
3430
大道如青天,协程来通信,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang通道channel的使用EP14
    众所周知,Go lang的作用域相对严格,数据之间的通信往往要依靠参数的传递,但如果想在多个协程任务中间做数据通信,就需要通道(channel)的参与,我们可以把数据封装成一个对象,然后把这个对象的指针传入某个通道变量中,另外一个协程从这个通道中读出变量的指针,并处理其指向的内存对象。
用户9127725
2022/09/23
2050
大道如青天,协程来通信,Go lang1.18入门精炼教程,由白丁入鸿儒,Go lang通道channel的使用EP14
GoLang协程与通道---上
作为一门 21 世纪的语言,Go 原生支持应用之间的通信(网络,客户端和服务端,分布式计算)和程序的并发。程序可以在不同的处理器和计算机上同时执行不同的代码段。Go 语言为构建并发程序的基本代码块是 协程 (goroutine) 与通道 (channel)。他们需要语言,编译器,和runtime的支持。Go 语言提供的垃圾回收器对并发编程至关重要。
大忽悠爱学习
2022/08/23
7960
GoLang协程与通道---上
A Bite of GoLang(下)
8. Goroutine 8.0、Goroutine介绍 协程 Coroutine 轻量级"线程" 上面的两个特征到底是什么意思呢?下面我们通过具体的事例详细的讲述一下, package main
盛国存
2018/05/14
1.1K1
A Bite of GoLang(下)
Go错误集锦 | 正确理解nil通道及其使用场景
在Go中有时候忘记使用nil通道也是经常犯的一个错误。本节我们一起来看看什么是nil通道,为什么要使用nil通道。
Go学堂
2023/01/31
3980
如何使用 Go 更好地开发并发程序,纯干货!
Go 语言的并发特性是其一大亮点,今天我们来带着大家一起看看如何使用 Go 更好地开发并发程序?
aoho求索
2021/03/16
5420
如何使用 Go 更好地开发并发程序,纯干货!
《快学 Go 语言》第 12 课 —— 神秘的地下通道
不同的并行协程之间交流的方式有两种,一种是通过共享变量,另一种是通过队列。Go 语言鼓励使用队列的形式来交流,它单独为协程之间的队列数据交流定制了特殊的语法 —— 通道。
老钱
2018/12/24
4080
《快学 Go 语言》第 12 课 —— 神秘的地下通道
「一闻秒懂」你了解goroutine和channel吗?
大家都知道进程是操作系统资源分配的基本单位,有独立的内存空间,线程可以共享同一个进程的内存空间,所以线程相对轻量,上下文切换开销也小。虽然线程已经比较轻量了,但还是占近1M的内存,而今天介绍的有“轻量级线程”之称的Goroutine,可以小至几十K甚至几K,切换的开销更小。
平也
2020/04/16
5120
「一闻秒懂」你了解goroutine和channel吗?
Golang学习笔记之并发.协程(Goroutine)、信道(Channel)
简单的理解一下,并发就是你在跑步的时候鞋带开了,你停下来系鞋带。而并行则是,你一边听歌一边跑步。 并行并不代表比并发快,举一个例子,当文件下载完成时,应该使用弹出窗口来通知用户。而这种通信发生在负责下载的组件和负责渲染用户界面的组件之间。在并发系统中,这种通信的开销很低。而如果这两个组件并行地运行在 CPU 的不同核上,这种通信的开销却很大。因此并行程序并不一定会执行得更快。 Go 原生支持并发。在Go中,使用 Go 协程(Goroutine)和信道(channel)来处理并发。
李海彬
2018/12/27
1.4K0
Golang学习笔记之并发.协程(Goroutine)、信道(Channel)
什么时候用Goroutine?什么时候用Channel?
什么场景下用channel合适呢? 通过全局变量加锁同步来实现通讯,并不利于多个协程对全局变量的读写操作。 加锁虽然可以解决goroutine对全局变量的抢占资源问题,但是影响性能,违背了原则。 总结:为了解决上述的问题,我们可以引入channel,使用channel进行协程goroutine间的通信。 Go语言中的操作系统线程和goroutine的关系: 一个操作系统线程对应用户态多个goroutine。 go程序可以同时使用多个操作系统线程。 goroutine和OS线程是多对多的关系,即m:n。 Go
王中阳Go
2022/10/26
9910
总结了才知道,原来channel有这么多用法!V2
这篇文章总结了channel的11种常用操作,以一个更高的视角看待channel,会给大家带来对channel更全面的认识。
大彬
2019/07/31
2.1K0
《Go小技巧&易错点100例》第二十八篇
在 Go 语言中,runtime.Caller(1) 是 runtime 包提供的一个函数,用于获取当前 goroutine 的调用堆栈中的特定调用者的信息。这里的 1 表示要跳过的调用帧数。具体来说,当你调用 runtime.Caller(1) 时,它会返回调用 runtime.Caller 的函数的调用者的信息。
闫同学
2025/01/23
680
go channel 管道
协程是并发编程的基础,而管道(channel)则是并发中协程之间沟通的桥梁,很多时候我们启动一个协程去执行完一个操作,执行操作之后我们需要返回结果,或者多个协程之间需要相互协作。
看、未来
2022/06/19
6940
go channel 管道
学会 Go select 语句,轻松实现高效并发
在 Go 语言中,Goroutine 和 Channel 是非常重要的并发编程概念,它们可以帮助我们解决并发编程中的各种问题。关于它们的基本概念和用法,前面的文章 一文初探 Goroutine 与 channel 中已经进行了介绍。而本文将重点介绍 select,它是协调多个 channel 的桥梁。
陈明勇
2023/10/15
8200
学会 Go select 语句,轻松实现高效并发
Golang channel 快速入门
Go 中多个 Goroutine 间的通信和同步一般使用 channel 来完成。channel 是有类型的管道,可以用 channel 操作符 <- 对其发送或者接收值。
恋喵大鲤鱼
2020/07/27
5800
Golang channel 快速入门
Go基础——channel通道
要想理解 channel 要先知道 CSP 模型。CSP 是 Communicating Sequential Process 的简称,中文可以叫做通信顺序进程,是一种并发编程模型,由 Tony Hoare 于 1977 年提出。简单来说,CSP 模型由并发执行的实体(线程或者进程)所组成,实体之间通过发送消息进行通信,这里发送消息时使用的就是通道,或者叫 channel。CSP 模型的关键是关注 channel,而不关注发送消息的实体。Go 语言实现了 CSP 部分理论,goroutine 对应 CSP 中并发执行的实体,channel 也就对应着 CSP 中的 channel。
羊羽shine
2019/05/28
7350
推荐阅读
相关推荐
go语言学习-并发编程
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档