3.1 goroutine

3.1 goroutine #

  • 发音:/ˈɡoʊruːtiːn/ (国际音标)"够-如-挺
    • “go” 发 /ɡoʊ/(类似中文"够"的发音)
    • “routine” 发 /ruːtiːn/(类似中文"如听"的快速连读)
  • 并发编程表现为 程序由若干个自主的活动单元组成,如Web服务器可以一次处理数千个请求;传统的批处理任务——读取数据、计算、将结果输出——也使用并发来隐藏I/O操作的延迟;CPU内核的个数每年变多,但是速度没什么变化;
  • 并发编程在本质上也比顺序编程要困难一些,从顺序编程获取的直觉让我们在并发编程上加倍地迷茫;学习之初可以暂时假设goroutine类似于操作系统的线程,goroutine和线程之间在数量上有非常大的差别;
  • Go有两种并发编程的风格:
    • goroutine、通道(channel):支持通信顺序进程(Communicating Sequential Process,CSP)的并发模式,在不同的执行体(goroutine)之间传递值,但是变量本身局限于单一的执行体;
    • 共享内存多线程的传统模型:和在其他主流语言中使用线程类似;
  • 当一个程序启动时,只有一个goroutine来调用main函数,称它为 main goroutine;
  • 新的goroutine通过在函数或方法调用前,加go关键字前缀进行创建。go语句使函数或方法在一个新创建的goroutine中调用,go语句本身的执行立即完成
    1
    2
    
    f()
    go f() // go语句本身的执行立即完成,并不等待f()的return
    
  • 示例:spinner.go
    • main函数返回时,所有的goroutine都暴力地直接终结,然后程序退出;
    • 除了从main返回或者退出程序之外,没有程序化的方法让一个goroutine来停止另一个,但有办法和goroutine通信来要求它自己停止;
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    
    func main() {    // main goroutine将计算菲波那契数列的第45个元素值。
        go spinner(100 * time.Millisecond)
        const n = 45
        fibN := fib(n) // slow
        fmt.Printf("\rFibonacci(%d) = %d\n", n, fibN) 
    func spinner(delay time.Duration) {
        for {
            for _, r := range `-\|/` {
                fmt.Printf("\r%c", r)
                time.Sleep(delay)
            }
        }
    }
    func fib(x int) int {
        if x < 2 {
            return x
        }
        return fib(x-1) + fib(x-2)
    }
    
  • **示例:**clock.go
    • 格式化模板限定为Mon Jan 2 03:04:05PM 2006 UTC-0700。有8个部分(周几、月份、一个月的第几天……)。可以以任意的形式来组合前面这个模板;出现在模板中的部分会作为参考来对时间格式进行输出。
    • 这是go语言和其它语言相比比较奇葩的一个地方。你需要记住格式化字符串是:1月2日下午3点4分5秒零六年UTC-0700**(记忆:1234567)**,而不像其它语言那样Y-m-d H:i:s一样
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      
      // Clock1 is a TCP server that periodically writes the time.
      package main
      func main() {
          listener, err := net.Listen("tcp", "localhost:8000")  // 监听8000端口
          if err != nil {
              log.Fatal(err)
          }
          for {
              conn, err := listener.Accept()  // Accept方法会直接阻塞,直到一个新的连接被创建,然后会返回一个net.Conn对象来表示这个连接
              if err != nil {
                  log.Print(err) // e.g., connection aborted
                  continue
              }
              go handleConn(conn) // 仅仅在 函数调用的地方**增加go关键字**,让每一次handleConn的调用都进入自己的一个独立的goroutine内执行
          }
      }
      func handleConn(c net.Conn) {
          defer c.Close()   // 关闭服务器侧的连接,然后返回到主函数,继续等待下一个连接请求
          for {   // 死循环会一直执行,直到写入失败,如可能的原因是客户端主动断开连接
      		    // 
              _, err := io.WriteString(c, time.Now().Format("\r15:04:05"))  // 由于net.Conn实现了io.Writer接口,我们可以直接向其写入内容。 \r (回车,Carriage Return,CR): 将光标回到当前行的行首(而不会换到下一行),之后的输出会把之前的输出覆盖
              if err != nil {
                  return // e.g., client disconnected
              }
              time.Sleep(1 * time.Second)
          }
      }
      
    • 阻塞执行/顺序编程:服务器顺序执行,第二个nc客户端接收不到时间;
    • 并发执行/并发编程:多个客户端可以同时接收到时间;
  • 示例: 并发的Echo服务reverb.go
    • go后跟的函数的参数表达式会在go语句自身执行时(main goroutine中)被求值
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    
    func echo(c net.Conn, shout string, delay time.Duration) {
        fmt.Fprintln(c, "\t", strings.ToUpper(shout))
        time.Sleep(delay)    // 由于这里设置了dalay,客户端多次发送不同的消息,所有echo的回显会顺序的执行,程序非常慢
        fmt.Fprintln(c, "\t", shout)
        time.Sleep(delay)
        fmt.Fprintln(c, "\t", strings.ToLower(shout))
    }
    func handleConn(c net.Conn) {
        input := bufio.NewScanner(c)
        for input.Scan() {
            go echo(c, input.Text(), 1*time.Second)    // 所有echo并发的执行,程序非常快
        }
        // NOTE: ignoring potential errors from input.Err()
        c.Close()
    }