Preface
Those who have heard of it go
must know go
that its biggest feature goroutine
is concurrent programming, and when it comes to concurrent programming, using it channel
for data transmission is go
a required course in it.
go
The concurrency philosophy: Don't communicate through shared memory, but implement memory sharing through communication.
channel
There are many pitfalls. This article will briefly talk about channel
the method of closing them.
Basic principles for closing channels
The widely circulated channel
principle of closure:
Do not close from the receiving end
channel
, and do not actively close when there are multiple senders.channel
The origin of this principle is because:
channel
Cannot send data to closed- Cannot re-close an already closed
channel
How to close
- The more crude method is to use
defer-recovery
the mechanism. If it is blocked when closingpanic
, it will also be blockedrecovery
. - Since
channel
can only be usedclose
once, thengo
the in the source code packagesync.Once
can come in handy, specifically for doing this kind of thing.
Next, according to the number of sender
and receiver
, the following situations are divided:
sender
Single Singlereceiver
- Single
sender
and multiplereceiver
- Long
sender
singlereceiver
sender
Manyreceiver
In cases 1 and 2, just sender
close it directly on the end channel
.
func main() {
dataCh := make(chan int, 100)
// sender
go func() {
for i := 0; i < 1000; i++ {
dataCh <- i + 1
}
log.Println("send complete")
close(dataCh)
}()
// receiver
for i := 0; i < 5; i++ {
go func() {
for {
data, ok := <-dataCh
if !ok {
// 已关闭
return
}
_ = data
}
}()
}
select {
case <-time.After(time.Second * 5):
fmt.Println(runtime.NumGoroutine())
}
}
In the third case, you can add a shutdown signal stopCh
and issue an instruction to close the data at receiver
the end . After listening to the shutdown signal, no more data is sent to the data.stopCh
dataCh
sender
dataCh
package main
import (
"time"
"math/rand"
"sync"
"log"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const Max = 100000
const NumSenders = 1000
wgReceivers := sync.WaitGroup{
}
wgReceivers.Add(1)
dataCh := make(chan int)
stopCh := make(chan struct{
})
// senders
for i := 0; i < NumSenders; i++ {
go func() {
for {
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- rand.Intn(Max):
}
}
}()
}
// receiver
go func() {
defer wgReceivers.Done()
for value := range dataCh {
if value == Max-1 {
close(stopCh)
return
}
log.Println(value)
}
}()
wgReceivers.Wait()
}
receiver
The fourth case is more complicated and cannot be closed directly on the end like the third case stopCh
. This will result in repeated closing of the closed channel
end panic
. Therefore, a middleman needs to be added toStop
to receive the shutdown stopCh
request.
package main
import (
"time"
"math/rand"
"sync"
"log"
"strconv"
)
func main() {
rand.Seed(time.Now().UnixNano())
log.SetFlags(0)
const Max = 100000
const NumReceivers = 10
const NumSenders = 1000
wgReceivers := sync.WaitGroup{
}
wgReceivers.Add(NumReceivers)
dataCh := make(chan int)
stopCh := make(chan struct{
})
// 这个是添加的中间人,通过它来接收关闭 stopCh 的请求,做一次关闭
// 这里给缓存是 goroutine 启动时机,可能导致 select 选择,导致逻辑问题
toStop := make(chan string, 1)
var stoppedBy string
go func() {
stoppedBy = <-toStop
close(stopCh)
}()
// senders
for i := 0; i < NumSenders; i++ {
go func(id string) {
for {
value := rand.Intn(Max)
if value == 0 {
select {
case toStop <- "sender#" + id:
default:
}
return
}
// 由于 select 是随机选择的,所以先在这里尝试得知是否关闭
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case dataCh <- value:
}
}
}(strconv.Itoa(i))
}
// receivers
for i := 0; i < NumReceivers; i++ {
go func(id string) {
defer wgReceivers.Done()
for {
select {
case <- stopCh:
return
default:
}
select {
case <- stopCh:
return
case value := <-dataCh:
if value == Max-1 {
select {
case toStop <- "receiver#" + id:
default:
}
return
}
log.Println(value)
}
}
}(strconv.Itoa(i))
}
wgReceivers.Wait()
log.Println("stopped by", stoppedBy)
}
This example can send a shutdown signal to both the sender
and receiver
end, toStop
pass the shutdown signal through this middleman, and close it after receiving it stopCh
. It should be noted here that toStop
it is defined as buffered channel
. If it is not buffered, it may happen that <-toStop
other coroutines have already sent toStop<-xx
shutdown signals to it before the receiving coroutine starts running. At this time, the statement may be taken
in the sender
or branch , resulting in logic errors.receiver
select
default
In this example, a simpler approach can be to toStop
set the cache to sender
the sum receiver
of and , which can be abbreviated as follows:
...
toStop := make(chan string, NumReceivers + NumSenders)
...
value := rand.Intn(Max)
if value == 0 {
toStop <- "sender#" + id
return
}
...
if value == Max-1 {
toStop <- "receiver#" + id
return
}
...
Notes on channels
channel
The declaration must usemake
the keyword, not directlyvar c chan int
, so what you get isnil channel
- Unable to
nil channel
send data to
var c chan int
c <- 1 // panic
- Closed
channel
No more data can be sent to it
c := make(chan int)
close(c)
c <- 1 // panic
- Cannot re-close an already closed
channel
c := make(chan int)
close(c)
close(c) // panic
- As long as you
channel
have no reference relationship, even if you have notclose
closed it orchannel
there is a large amount of accumulated data that has not been consumed, it will eventually begc
released.
Summarize
channel
Basic rules for closing :
- In single
sender
case, it cansender
be closed directly on the client sidechannel
. - In many
sender
cases, you can add a shutdown signal thatchannel
is specifically used to shut down data transmissionchannel
.
Principle: Do not close from the receiving end channel
, and do not actively close when there are multiple senderschannel
.
Essence: What has been closed channel
cannot be closed again (or data can be sent to it again) .
channel
The use of is very diverse. This article lists several basic scenarios. If you really want to understand it, you still have to write more and think more.