The outline
[toc]
Original public account: strange cloud storage
Why does Golang not judge close?
When you first learned Golang Chan, you probably encountered the “Send on closed Channel” panic. This panic is triggered when you intend to send an element to a close channel. So when you first encountered this problem did you ever wonder if a channel could provide an interface method to tell if it was close? I thought about this, but I looked through Chan’s source code and couldn’t find it. Why is that?
Let me hold this question first, and let’s go through some things related to channel close, mainly thinking about three questions:
- What does closing a channel do?
- How to avoid panic caused by close channel?
- How to gracefully close a channel?
What does closing a channel do?
First, the user can close the channel as follows:
c := make(chan int)
// ...
close(c)
Copy the code
Close a channel using GDB or delve. The compiler converts it to a closechan function, which is a complete implementation of the close channel. We can analyze it.
closechan
The corresponding compile function is Closechan, which is simple and does about three things:
- Mark position 1, that is
c.closed = 1
; - Free resources and wake up coroutines for all waiting elements;
- Free resources and wake up coroutines for all elements waiting to be written;
func closechan(c *hchan) {
// The following is the operation inside the lock
lock(&c.lock)
// Do not repeat close a channel, otherwise panic
ifc.closed ! =0 {
unlock(&c.lock)
panic(plainError("close of closed channel"))}// Location 1 of the closed flag
c.closed = 1
var glist gList
// Release the waiter resource for all waiting elements
for {
// The waiter waiting to read is out of line
sg := c.recvq.dequeue()
// Destroy resources one by one
ifsg.elem ! =nil {
typedmemclr(c.elemtype, sg.elem)
sg.elem = nil
}
gp := sg.g
gp.param = nil
// The corresponding goroutine is added to the unified queue
glist.push(gp)
}
// Release the waiter resources for all elements waiting to be written (they will panic later)
for {
// Wait for the waiter to leave the queue
sg := c.sendq.dequeue()
// Destroy resources one by one
sg.elem = nil
gp := sg.g
gp.param = nil
// Add the corresponding goroutine to the unified queue
glist.push(gp)
}
unlock(&c.lock)
// Wake up all the "waiter" coroutines (the list of coroutines pushed above)
for! glist.empty() { gp := glist.pop() gp.schedlink =0
goready(gp, 3)}}Copy the code
Through the above code logic, we peek into two important messages:
- Close chan has an identifier bit;
- Close Chan is what awakens those who are waiting;
However, it is strange that golang does not provide an interface to determine whether Chan is closed. Can we implement a method to determine if chan is close?
A function that determines if chan is close
How? First, the isChanClose function has a few requirements:
- Capable of indicating that it is indeed close;
- Runs properly at all times and returns (non-blocking);
Recalling the golang Channel’s most detailed analysis chapter, and considering the functions related to Send and recv, we can see that the current channel is given to the user in essentially two ways: read and write, and our implementation of isChanClose can only do this on top of that.
- Write:
c <- x
- Read:
<-c
或v := <-c
或v, ok := <-c
One way to think about it: by “writing” chan
“Write” is definitely not a judgment. In order to determine whether chan is close or not, I will try to write data into it. This will cause a direct panic in chansend, as follows:
func chansend(c *hchan, ep unsafe.Pointer, block bool, callerpc uintptr) bool {
/ /...
// Processing logic after channel close
ifc.closed ! =0 {
unlock(&c.lock)
panic(plainError("send on closed channel"))}/ /...
}
Copy the code
Of course, you can do this technically if you are wild, because panic can be captured, but this is too wild to recommend.
Way to think about it: by “reading” chan
“Read”. The parsing function chanrecv knows that when it attempts to read data from a chan that is already close, it returns (selected=true, received=false), Received = false to see if a channel is close. Chanrecv has the following code:
func chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool) {
// ...
// Processing logic after channel close
ifc.closed ! =0 && c.qcount == 0 {
unlock(&c.lock)
ifep ! =nil {
typedmemclr(c.elemtype, ep)
}
return true.false
}
// ...
}
Copy the code
So, we now know that we can tell by the “read” effect, but we can’t just write it like this:
// Error examples
func isChanClose(ch chan int) bool {
_, ok := <- c
}
Copy the code
The above is an example of an error, because _, ok := <-c compenses chanrecv2, this function is passed with the value true, so this function is blocked when c is normal, so this cannot be used as a normal function call, because the coroutine will get stuck. How to solve this problem? This can be solved by combining select and <-chan, which together correspond to selectNbRecv and selectNbrecv2 functions, which are non-blocking (block = false).
Correct example:
func isChanClose(ch chan int) bool {
select {
case _, received := <- ch:
return! receiveddefault:}return false
}
Copy the code
Many people on the Internet give an example of an isChanClose error.
func isChanClose(ch chan int) bool {
select {
case <- ch:
return true
default:}return false
}
Copy the code
Think about it: Why is the first example right and the second wrong?
Because the first example is compiled as selectNbrecv2, and the second example is compiled as selectNbrecv1. The difference between the two is that selectNbrecv2 returns one more received argument. Only this function can indicate whether the element was successfully dequeued, while selected simply determines whether to enter the select Case branch. We use the received return value to infer whether chan is close or not. We use the received return value to infer whether chan is close or not.
Summary:
- The code for case must be
_, received := <- ch
If only in the form of<- ch
Is wrong logic, because we’re focusing onreceived
The value of the; - Select must have a default branch, otherwise it will block the function. We need to ensure that the function returns properly;
Chan, the principle of close
- Never try to close a channel on the reader side. The writer side has no way of knowing if the channel is closed. Writing to a closed channel will panic.
- A writer-side where the writer-side can safely close the channel;
- Do not close a channel on the writer-side when there are multiple writer-sides. Other writer-sides will not know if the channel is closed. Closing a closed channel will cause a panic (you need to make sure that only one person calls close).
- When a channel is used as an argument to a function, it is best to take a direction;
In fact, these principles only have one point: if it must be safe to go to the close channel.
You don’t have toisChanClose
Function!!!!!!
If isChanClose returns false, you think the channel is still normal. If isChanClose returns false, you think the channel is normal. However, the next time the channel is closed, a panic will occur if the data is “written” to it, as follows:
if isChanClose( c ) {
// Close the scene, exit
return
}
// Do not close the scenario, continue to execute (may still panic)
c <- x
Copy the code
Because there is still a time window after the judgment, the application of isChanClose is still limited, so is there a better way?
If a channel is closed or not, you can use it safely. If a channel is closed, you can avoid panic.
The essence of this problem is to ensure the timing of an event. The official recommendation is to use context in conjunction with this. We can use a CTX variable to indicate the close event, rather than directly determine the state of a channel. Here’s an example:
select {
case <-ctx.Done():
// ... exit
return
case v, ok := <-c:
// do something....
default:
// do default ....
}
Copy the code
After the ctx.done () event occurs, we explicitly do not read the channel’s data.
or
select {
case <-ctx.Done():
// ... exit
return
default:
// push
c <- x
}
Copy the code
After the ctx.done () event occurs, we explicitly do not write data to or read data from the channel, so ensure this timing. There must be no problem.
We just need to make sure that:
- Done() = ctx.done () = ctx.done () = ctx.done () = ctx.done () = ctX.done () = ctX.done ();
- Only this timing ensures that everything is safe when the Done event is known;
- Select * from case where ctx.done () is performed first; otherwise, it is possible that the chan operation was performed first, causing panic;
How to gracefully shut down Chan?
Method 1: panic-recover
To close a channel, call close, but to close an already closed channel will result in a panic. Panic -recover can be used together.
func SafeClose(ch chan int) (closed bool) {
defer func(a) {
if recover() != nil {
closed = false
}
}()
// If ch is already closed, panic will occur and will be caught by recover;
close(ch)
return true
}
Copy the code
It’s not elegant.
Method 2: sync.once
You can use sync.once to ensure that close is executed only Once.
type ChanMgr struct {
C chan int
once sync.Once
}
func NewChanMgr(a) *ChanMgr {
return &ChanMgr{C: make(chan int)}}func (cm *ChanMgr) SafeClose(a) {
cm.once.Do(func(a) { close(cm.C) })
}
Copy the code
That looks okay.
Method three: event synchronization to solve
There are two simple rules for closing a channel:
- Never attempt to close a channel on the read side;
- Always allow only one Goroutine (i.e., a goroutine used only for closing) to perform the closing operation;
You can use Sync.waitGroup to synchronize the closing event, following the above principles. Here are a few examples:
First example: a sender
package main
import "sync"
func main(a) {
// Channel initialization
c := make(chan int.10)
// For recevivers to synchronize events
wg := sync.WaitGroup{}
// Sender (write side)
go func(a) {
/ / team
c <- 1
// ...
// Close the channel if certain cases are met
close(c)
}()
// Receivers (read side)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(a) {
defer wg.Done()
/ /... Processing data in a channel
for v := range c {
_ = v
}
}()
}
// Wait for all Receivers to complete;
wg.Wait()
}
Copy the code
In this case, we’re closing the channel in the sender’s goroutine, because there’s only one sender, so it’s safe to close it. The Receiver uses WaitGroup to synchronize events, the Receiver’s for loop exits only after channel close, and the main coroutine’s Wg.wait () statement returns only when all Receivers have completed. So, the sequence of events is:
- The write side joins an integer element
- Shut down the channel
- All read ends exit safely
- The main coroutine returns
Everything is safe.
Second example: Multiple sender
package main
import (
"context"
"sync"
"time"
)
func main(a) {
// Channel initialization
c := make(chan int.10)
// For recevivers to synchronize events
wg := sync.WaitGroup{}
/ / context
ctx, cancel := context.WithCancel(context.TODO())
// Coroutines that are specifically closed
go func(a) {
time.Sleep(2 * time.Second)
cancel()
/ /... Under certain conditions, close the channel
close(c)
}()
// senders (write side)
for i := 0; i < 10; i++ {
go func(ctx context.Context, id int) {
select {
case <-ctx.Done():
return
case c <- id: / / team
// ...
}
}(ctx, i)
}
// Receivers (read side)
for i := 0; i < 10; i++ {
wg.Add(1)
go func(a) {
defer wg.Done()
/ /... Processing data in a channel
for v := range c {
_ = v
}
}()
}
// Wait for all Receivers to complete;
wg.Wait()
}
Copy the code
In this example, we see multiple sender and receiver. In this case, we need to make sure that close(ch) can only be done by one person. We need to separate out a goroutine to do this, and use context to synchronize the events.
- 10 writer-side coroutines run, delivering elements;
- Ten read-side coroutines (receivers) run to read elements;
- After the 2-minute timeout, the separate coroutine executes
close(channel)
Operation; - The main coroutine returns;
Everything is safe.
conclusion
- Channel does not directly provide an interface to judge whether a channel is close. Officially, it is recommended to use context and SELECT syntax together to notify the event to achieve the elegant effect of judging whether a channel is closed.
- Never try to close a channel on the read side, always maintain a close entry point, use Sync. WaitGroup and context to synchronize events, and achieve elegant close effect;