塞完数据之后close(chan)会怎样

简介: 塞完数据之后close(chan)会怎样

塞完数据之后close(chan)会怎样



一、close(chan)之后程序继续读取会怎样


看代码


type taskFunc func(ctx context.Context) error
func Run(ctx context.Context, tasks ...taskFunc) (e error) {
 ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
 defer cancel()
  // 开辟tasks长度的buf chan
 queue := make(chan taskFunc, len(tasks)) 
 for _, task := range tasks {
  queue <- task
 }
  // 关闭chan ???这里关闭 对后面读取有影响吗
 close(queue)
 numCPU := runtime.NumCPU()
 wg := sync.WaitGroup{}
 wg.Add(numCPU)
 for i:=0; i<numCPU; i++ {
  go func() {
   defer wg.Done()
   for {
    select {
    case task, ok := <- queue: // 读取
     if !ok {
      fmt.Println("chan closed!")
      return
     }
     if ctx.Err() != nil {
      fmt.Println("context error")
      return
     }
     if err := task(ctx); err != nil {
      e = err
      fmt.Println("task run error: ", err)
      return
     }
    case <-ctx.Done():
     e = ctx.Err()
     fmt.Println("Done !")
     return
    }
   }
  }()
 }
 wg.Wait()
 fmt.Println("end")
 return nil
}
func T1(ctx context.Context) error {
 return nil
}
func T2(ctx context.Context) error {
 return nil
}
func T3(ctx context.Context) error {
 time.Sleep(10*time.Second)
 return errors.New("T3 超时了")
}
func T4(ctx context.Context) error {
 return errors.New("T4 error")
}
func main() {
   Run(context.Background(), T1, T2,T3, T4)
}


测试:


import (
 "context"
 "fmt"
 "go.uber.org/goleak"
 "testing"
)
func TestMain(m *testing.M)  {
 fmt.Println("start")
 goleak.VerifyTestMain(m)
}
func TestRun(t *testing.T) {
 Run(context.Background(), T1, T2,T3, T4)
}


输出:


start
=== RUN   TestRun
chan closed!
chan closed!
chan closed!
chan closed!
task run error:  T4 error
chan closed!
chan closed!
task run error:  T3 超时了
done
--- PASS: TestRun (10.00s)

发现程序正常运行输出结果,这是为什么呢?


二、原因在select的接受处理上


看select源码发现,selectgo在循环处理case上有独特的顺序,且看:


loop:
    for i := 0; i < ncases; i++ {
        // 根据 `pollorder` 记录的随机 scases 索引来获取 cas
        casi = int(pollorder[i])
        cas = &scases[casi]
        c = cas.c
        switch case.kind {
        case caseNil:
            continue
        case caseRecv:
            // ...
        case caseSend:
            // ...
        case caseDefault:
            // ...
        }
    }
    // ...
}


根据pollorder记录的随机scases索引来遍历处理case,然后根据case.kind来查看channel是否准备好,然后goto跳转到相应逻辑。


case.kindcaseNil,说明channelnil,那么continue,不进行任何处理。

caseRecv: channel接收操作:


switch case.kind {
        case caseRecv:
            sg = c.sendq.dequeue()
            if sg != nil {
                goto recv
            }
            if c.qcount > 0 {
                goto bufrecv
            }
            if c.closed != 0 {
                goto rclose
            }
        ...
        }
  • 看第一个if:如果channel中有待发送的goroutine, 跳转到recv,调用recv完成接收操作。
  • 看第二个if:如果channel中有缓冲数据,那么跳转到bufrecv,从缓冲区中获取数据。
  • 看第三个if:如果channel已关闭,跳转到rclose, 将接收值置为空值,recvOK置为false


看到了吧,在recv的时候,判断接受操作优先于判断关闭操作,所以我们上述程序在塞完task之后就直接关闭chan


三、小结


掌握channelfor select用法至关重要,稍不留意就会造成程序bug,所以平时多看看源码,想一想为什么。

相关文章
|
2月前
|
网络协议 Linux API
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
Linux网络编程:shutdown() 与 close() 函数详解:剖析 shutdown()、close() 函数的实现原理、参数说明和使用技巧
91 0
|
5月前
npm ERR! code ERR_STREAM_WRITE_AFTER_END npm ERR! write after en
npm ERR! code ERR_STREAM_WRITE_AFTER_END npm ERR! write after en
|
8月前
|
存储 C++
close()关闭文件方法
我们知道,调用 open() 方法打开文件,是文件流对象和文件之间建立关联的过程。那么,调用 close() 方法关闭已打开的文件,就可以理解为是切断文件流对象和文件之间的关联。注意,close() 方法的功能仅是切断文件流与文件之间的关联,该文件流并会被销毁,其后续还可用于关联其它的文件。 close() 方法的用法很简单,其语法格式如下: void close( ) 可以看到,该方法既不需要传递任何参数,也没有返回值。 举个例子: #include <fstream> using namespace std; int main() {
86 0
|
9月前
|
Go
Go使用chan或context退出协程
Go使用chan或context退出协程
156 1
|
物联网 Linux 开发者
open_close 函数|学习笔记
快速学习 open_close 函数
106 0
|
Go
go channel缓冲区的大小
go channel缓冲区的大小 len也可以作用于channel,代表现在channel缓冲区中还有多少数据没有读取.示例如下 c:=make(chan int,20) fmt.Println("len:",len(c)) //0 c
2805 0
|
网络协议 C语言 Windows
关于close和shutdown
我们知道TCP是全双工的,可以在接收数据的同时发送数据。假设有主机A在和主机B通信,可以认为是在两者之间存在两个管道。就像这样:A ---------> BA
796 0