• Go语言 并发与通道


    Go语言

    13. 并发与通道

    并发是指在同一段时间内,程序可以执行多个任务。

    Go语言如何利用协程(coroutine)和通道(channel)来解决并发问题。

    13.1 概述

    在程序中往往有很多很耗时的工作,比如上传文件、下载文件、网络聊天。这时候,一个线程是服务不了多个用户的,会产生因为资源独占导致的等待问题,这时候就需要使用并发的手段来解决。

    协程(coroutine),属于多线程编程。

    13.1.1 并行与并发

    并行(parallelism):指在同一时刻,有多条指令在多个处理器上同时执行。

    并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速地轮换执行,得到在宏观上有多个进程同时执行的效果,但在微观上并不是同时执行,只是把时间片分成了若干段,使得多个进程快速交替执行。

    并行是真正意义上的同时执行,而并发只是从宏观的角度来看具有同时执行的效果。

    13.1.2 Go并发优势

    Go语言的优势在于从语言层面上支持了并发,并发编程的内存管理有时是非常复杂的,而开发者不用担心并发的底层逻辑、内存管理,因为这些在语言层面上已经解决了,开发者只需要编写好自己的业务逻辑即可。Go语言也提供了十分强大的自动垃圾回收机制,开发者不用担心创建的量如何销毁。

    在Go语言里,只需要使用“go”加上函数名称就可以让这个函数变为并发函数。

    package main
    
    func run(arg string) {
    }
    
    func main() {
       go run("This is new thread")
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Go语言的并发基于CSP(Communication Sequential Process,通信顺序进程)模型,CSP模型是在20世纪70年代提出的用于描述两个独立的并发实体通过共享的通信管道(channel)进行通信的并发模型。

    简单来说,CSP模型提倡通过通信来共享内存,而非通过共享内存来通信。

    Go语言不是通过锁的方式,而是通过通信的方式,通过安全的通道发送和接收数据以实现同步,这就大大简化了并发编程的编写。

    13.2 goroutine

    goroutine是Go并发设计的核心,也叫协程,它比线程更加轻量,因此可以同时运行成千上万个并发任务。不仅如此,Go语言内部已经实现了goroutine之间的内存共享,它比线程更加易用、高效和轻便。

    13.2.1 goroutine定义

    在Go语言中,每一个并发的执行单元叫作一个goroutine。想要编写一个并发任务,只需要在调用的函数前面添加go关键字,就能使这个函数以协程的方式运行。

    go 函数名(函数参数)
    
    • 1

    如果函数有返回值,返回值会被忽略。因此,一旦使用go关键字,就不能使用函数返回值来与主进程进行数据交换,只能使用通道。

    对用户来说,协程与线程几乎没什么区别,但是实际上二者是有一定区别的。线程有固定的栈,基本都是2 MB,都是固定分配的;这个栈用于保存局部变量,在函数切换时使用。但是对于goroutine这种轻量级的协程来说,一个大小固定的栈可能会导致资源浪费,所以Go采用了动态扩张收缩的策略,初始化为2 KB,最大可扩张到1 GB。

    每个线程都有一个id,这个在线程创建时就会返回,所以可以很方便地通过id操作某个线程。但是goroutine内没有这个概念,这是在Go语言设计之初出于防止被滥用的考虑而定的,所以不能在一个协程中“杀死”另外一个协程,编码时需要考虑到协程什么时候创建以及什么时候释放。

    协程和线程最重要的区别:

    • 线程切换需要陷入内核,然后进行上下文切换,而协程在用户态由协程调度器完成,不需要陷入内核,这样代价就小了
    • 协程的切换时间点是由调度器决定,而不是由系统内核决定的,尽管它们的切换点都是时间片超过一定阈值,或者是进入I/O或睡眠等状态时
    • Go实现了垃圾回收,但垃圾回收的必要条件是内存位于一致状态,因此就需要暂停所有的线程。如果交给系统去做,那么会暂停所有的线程使其一致;但如果在Go里,调度器知道什么时候内存位于一致状态,也就没有必要暂停所有运行的线程。
    13.2.2 创建goroutine

    只需要在函数调用语句前面添加go关键字,就可以创建并发执行单元。开发人员无须了解任何执行细节,调度器会自动将其安排到合适的系统线程上去执行。

    示例

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func Task1() {
       for {
          fmt.Println(time.Now().Format("15:04:05"), "正在处理Task1的任务!")
          time.Sleep(time.Second * 3)
       }
    }
    
    func Task2() {
       for {
          fmt.Println(time.Now().Format("15:04:05"), "正在处理Task2的任务!")
          time.Sleep(time.Second * 1)
       }
    }
    
    func main() {
       go Task1()
       go Task2()
       for {
          fmt.Println(time.Now().Format("15:04:05"), "正在处理主进程的任务!")
          time.Sleep(time.Second * 2)
       }
    }
    
    • 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
    • 28
    • 29

    在这里插入图片描述

    如果把go关键字删掉,很显然,Task1 → 陷入死循环

    13.2.3 main函数

    当一个程序启动时,其主函数即在一个单独的goroutine中运行,我们称之为main goroutine。新的goroutine会使用go来创建。

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func Task1() {
       for {
          fmt.Println(time.Now().Format("15:04:05"), "正在处理Task1的任务!")
          time.Sleep(time.Second * 3)
       }
    }
    
    func main() {
       go Task1()
       time.Sleep(time.Second * 10)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    在这里插入图片描述

    可以看出,10秒后,所有的goroutine在main函数结束时,一并结束了。

    goroutine虽然类似于线程概念,但调度性能上不如线程细致,而细致程度取决于goroutine调度器的实现和运行环境。终止goroutine最好的方法是直接在函数中自然返回。

    13.2.4 使用匿名函数创建goroutine
    go func(参数列表) {
        函数体
    } (调用参数列表)
    
    • 1
    • 2
    • 3
    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       go func() {
          for {
             fmt.Println(time.Now().Format("15:04:05"), "正在处理Task1的任务!")
             time.Sleep(time.Second * 3)
          }
       }()
       time.Sleep(time.Second * 8)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    13.2.5 runtime包

    Go语言中runtime(运行时)包实现了一个小型的任务调度器。这个调度器的工作原理和系统对线程的调度类似,Go语言调度器可以高效地将CPU资源分配给每一个任务。

    1. Gosched()

      func Gosched()
      
      • 1

      Gosched()使当前Go协程放弃处理器,以让其他Go协程运行。它不会挂起当前Go协程,因此当前Go协程未来会恢复执行。

      Go语言的协程是抢占式调度的,当遇到长时间执行或者进行系统调用时,会主动把当前goroutine的CPU §转让出去,让其他goroutine能被调度并执行。一般出现如下几种情况,goroutine就会发生调度:

      • syscall

      • C函数调用

      • 主动调用runtime.Gosched

      • 某个goroutine的调用时间超过100ms,并且这个goroutine调用了非内联的函数

        【内联函数是指当编译器发现某段代码在调用一个内联函数时,它不是去调用该函数,而是将该函数的代码整段插入到当前位置。这样做的好处是省去了调用的过程,加快程序运行速度。】

      示例

      package main
      
      import "fmt"
      
      func main() {
         go func() {
            for i := 0; i < 3; i++ {
               fmt.Println("go")
            }
         }()
      
         for i := 0; i < 2; i++ {
            fmt.Println("main")
         }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15

      在这里插入图片描述

      由于main函数中的第二个for循环一直抢占了CPU控制权,直到运行结束也没有发生goroutine调度,因此,第一个Go协程匿名函数无法获取CPU控制权,导致自始至终都无法执行

      如果想要第一个for循环执行,可以在第二个for循环中使用runtime.Gosched()来阻止其获取控制权

      package main
      
      import (
         "fmt"
         "runtime"
      )
      
      func main() {
         go func() {
            for i := 0; i < 3; i++ {
               fmt.Println("go")
            }
         }()
      
         for i := 0; i < 2; i++ {
            runtime.Gosched()
            fmt.Println("main")
         }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

      在这里插入图片描述

    2. Goexit()

      func Goexit()
      
      • 1

      Goexit()终止调用它的Go协程,但其他Go协程不会受影响。Goexit()会在终止该Go协程前执行所有defer的函数。

      在程序的main go协程调用本函数时,会终结该Go协程,而不会让main返回。这是因为main函数没有返回,程序会继续执行其他的Go协程。如果所有其他Go协程都退出了,程序就会崩溃。

      示例

      package main
      
      import (
         "fmt"
         "runtime"
         "time"
      )
      
      func Task1() {
         defer fmt.Println("task1 stop")
         fmt.Println("task1 start")
         fmt.Println("task1 work")
      }
      
      func Task2() {
         defer fmt.Println("task2 stop")
         fmt.Println("task2 start")
         runtime.Goexit() //效果和return一样
         fmt.Println("task2 work")
      }
      
      func main() {
         go Task1()
         go Task2()
         time.Sleep(time.Second * 5)
      }
      
      • 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

      在这里插入图片描述

    3. GOMAXPROCS()

      GOMAXPROCS(n int)函数可以设置程序在运行中所使用的CPU数,在以后的编程中是用得最多的。Go语言程序默认会使用最大CPU数进行计算。

      func GOMAXPROCS(n int) int
      
      • 1

      GOMAXPROCS()设置可同时执行的最大CPU数,并返回先前的设置。若n < 1,它就不会更改当前设置。本地机器的逻辑CPU数可通过NumCPU查询。本函数在调度程序优化后会去掉。

      示例

      package main
      
      import (
         "fmt"
         "runtime"
         "time"
      )
      
      func main() {
         n := runtime.GOMAXPROCS(1)
         fmt.Println("先前的CPU核数设置为:", n)
      
         last := time.Now()
         for i := 0; i < 100000; i++ {
            go func() {
               //耗时任务
               a := 999999 ^ 999999
               a = a + 1
            }()
         }
         now := time.Now()
         fmt.Println(now.Sub(last))
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23

      在这里插入图片描述

      在这里插入图片描述

    13.3 channel

    goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。引用类型channel是CSP模式的具体体现,用于多个goroutine之间的通信。其内部实现了同步,确保并发安全。

    13.3.1 channel类型

    channel是一种特殊的类型,和map类似,channel也是一个对应make创建的底层数据结构的引用。

    var 通道变量 chan 通道类型
    
    • 1

    通道变量是保存通道的引用变量;通道类型是指该通道可传输的数据类型。当我们复制一个channel或用于函数参数传递时,我们只是拷贝了一个channel引用,因此调用者与被调用者都将引用同一个对象。和其他引用类型一样,channel的零值也是nil。

    定义一个channel时,也需要定义发送到channel的值的类型。

    make(chan Type) // 等价于make(chan Type, 0)
    make(chan Type, capacity)
    
    • 1
    • 2

    当capacity为0时,channel是无缓冲阻塞读写的;当capacity大于0时,channel是有缓冲、非阻塞的,直到写满capacity个元素才阻塞写入。

    channel通过操作符“<-”来接收和发送数据。

    channel <- value //发送value到channel
    <-channel //接收并将其丢弃
    x := <-channel //从channel中接收数据,并赋值给x
    x, ok := <-channel //同上,并检查通道是否关闭,将此状态赋值给ok
    
    • 1
    • 2
    • 3
    • 4

    默认情况下,channel接收和发送数据都是阻塞的,除非另一端已准备好接收,这样就使得goroutine的同步更加简单,而不需要显式锁。

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ch := make(chan string)
    
       go func() {
          fmt.Println(<-ch)
       }()
       ch <- "test"
       time.Sleep(time.Second)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    在这里插入图片描述

    程序定义并创建了一个可以传输string类型的ch通道变量,在匿名协程函数中,从ch通道中接收数据并打印。

    13.3.2 缓冲机制

    channel按是否支持缓冲区可分为无缓冲的通道(unbuffered channel)和有缓冲的通道(buffered channel)。

    无缓冲的通道是指在接收前没有能力保存任何值的通道。

    这种类型的通道要求发送goroutine和接收goroutine同时准备好,才能完成发送和接收操作。如果两个goroutine没有同时准备好,会导致先执行发送或接收操作的goroutine阻塞等待。

    这种对通道进行发送和接收的交互行为本身就是同步的,其中任意一个操作都无法离开另一个操作单独存在。

    无缓冲的channel创建格式:

    make(chan Type) //等价于make(chan Type, 0)
    
    • 1

    如果没有指定缓冲区容量,那么该通道就是同步的。

    示例

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ch := make(chan int, 0)
       go func() {
          for i := 0; i < 3; i++ {
             fmt.Printf("len(ch) = %v,cap(ch) = %v\n", len(ch), cap(ch))
             ch <- i
          }
       }()
       for i := 0; i < 3; i++ {
          time.Sleep(time.Second)
          fmt.Println(<-ch)
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    在这里插入图片描述

    程序创建了一个无缓冲通道ch,由于无缓冲,因此只有当接收者收到了数据,发送者才能继续发送数据。

    有缓冲通道是一种在被接收前能存储一个或多个值的通道。

    make(chan Type, capacity)
    
    • 1

    这种类型的通道并不强制要求goroutine之间必须同时完成接收和发送。通道阻塞发送和接收的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。

    这导致有缓冲的通道和无缓冲的通道之间有一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进行数据交换,有缓冲的通道没有这种保证。

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ch := make(chan int, 3)
       go func() {
          for i := 0; i < 3; i++ {
             fmt.Printf("len(ch) = %v,cap(ch) = %v\n", len(ch), cap(ch))
             ch <- i
          }
       }()
    
       for i := 0; i < 3; i++ {
          time.Sleep(time.Second)
          fmt.Println(<-ch)
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    在这里插入图片描述

    由于存在缓冲区(容量为3),在缓冲区未填满的情况下,程序就不会被阻塞执行

    13.3.3 close和range

    当发送者知道没有更多的值需要发送到channel时,让接收者也能及时知道没有更多的值需要接收是很有必要的,因为这样就可以让接收者停止不必要的等待。这可以通过内置的close函数和range关键字来实现。

    1. close

      使用close关闭channel时需要注意:

      • hannel不像文件一样需要经常去关闭,只有当你确实没有任何需要发送的数据时,或者想要显式地结束range循环之类的,才会去关闭channel。
      • 关闭channel后,无法向channel再次发送数据,再次发送将会引发panic错误。
      • 关闭channel后,可以继续从channel接收数据。
      • 对于nil channel,无论接收还是发送都会被阻塞。

      示例

      package main
      
      import (
         "fmt"
      )
      
      func main() {
         ch := make(chan int, 3)
         go func() {
            for i := 0; i < 3; i++ {
               fmt.Printf("len= %v,cap=%v\n", len(ch), cap(ch))
               ch <- i
            }
            close(ch)
         }()
      
         for {
            if val, ok := <-ch; ok == true {
               fmt.Println(val)
            } else {
               return
            }
         }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19
      • 20
      • 21
      • 22
      • 23
      • 24

      在这里插入图片描述

      发送者在发送完数据后,使用close关闭了通道,通道被关闭后,ok的false就会变为false,最终程序结束运行

    2. range

      使用range关键字遍历通道

      package main
      
      import "fmt"
      
      func main() {
         ch := make(chan int, 3)
         go func() {
            for i := 0; i < 3; i++ {
               fmt.Printf("len=%v,cap=%v\n", len(ch), cap(ch))
               ch <- i
            }
            close(ch)
         }()
      
         for data := range ch {
            fmt.Println(data)
         }
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18

      在这里插入图片描述

    13.3.4 单向channel

    默认情况下,通道是双向的,就是既可以向通道发送数据,也可以从通道中接收数据。但是,建一个通道作为参数进行传递,经常希望只单向使用,即要么只发送数据,要么只接收数据,这时候就可以指定通道的方向,也就是使用单向通道。

    单向channel变量的声明:

    var ch1 chan int // ch1为一个双向通道
    var ch2 chan<- int // ch2为一个只能接收的单向通道
    var <-chan int // ch3为一个只能发送的单向通道
    
    • 1
    • 2
    • 3

    “chan<-”表示数据进入通道,只要把数据写入通道,对于调用者而言就是输出。“<-chan”表示数据从通道中出来,对于调用者就是得到通道的数据,也就是输入。

    生产者–消费者示例

    package main
    
    import "fmt"
    
    func producer(out chan<- int) {
       for i := 0; i < 10; i++ {
          out <- i
       }
       close(out)
    }
    
    func consumer(in <-chan int) {
       for val := range in {
          fmt.Printf("消费者消费了%d\n", val)
       }
    }
    
    func main() {
       ch := make(chan int)
       go producer(ch)
       consumer(ch)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    在这里插入图片描述

    可以将普通的双向channel隐式转换为单向channel,不能将单向channel转换为双向channel。

    13.3.5 定时器

    在Go语言标准库的time包中,定时器的实现使用的就是单向channel。

    使用定时器实现每隔一段时间打印一串字符

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ticker := time.NewTicker(time.Second)
    
       for {
          <-ticker.C
          fmt.Println("loop")
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    每隔1s

    在这里插入图片描述

    13.4 select

    Go语言中,通过关键字select可以监听channel上的数据流动。select的用法和switch非常相似,由select开始一个新的选择块,每个选择条件由case语句来描述。

    13.4.1 select作用

    与switch语句可以选择任何可使用相等比较的条件相比,select有较多的限制,其中最大的限制就是每个case语句里面必须是一个I/O操作。

    select {
    case <-chan1:
        // 如果chan1成功读到数据,则执行该case语句
    case chan2 <- 1:
        // 如果成功向chan2写入数据,则执行该case语句
    default:
        //如果上面的case都没有执行成功,则执行该default语句
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    在一个select语句中,Go语言会按顺序从头至尾评估每一个发送和接收语句,如果其中的任意一个语句可以继续执行(没有被阻塞),那么就从那些可以执行的语句中随机选择一条来使用。

    如果没有任何一条语句可以执行(即所有通道都被阻塞),就会默认执行default语句,同时程序的执行会从select语句后的语句中恢复。如果没有default语句,则select语句将被阻塞,直到有一个channel可以进行下去。

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ch := make(chan int)
       go func() {
          for i := 0; i < 3; i++ {
             ch <- i
          }
       }()
    
       for {
          select {
          case msg := <-ch:
             fmt.Println(msg)
          default:
             time.Sleep(time.Second)
          }
       }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在这里插入图片描述

    一直等着

    13.4.2 超时

    有时候会出现goroutine阻塞的情况,为了避免程序长时间进入阻塞,我们可以使用select来实现阻塞超时机制。

    package main
    
    import (
       "fmt"
       "time"
    )
    
    func main() {
       ch := make(chan int)
       done := make(chan bool)
    
       go func() {
          for {
             select {
             case val := <-ch:
                fmt.Println(val)
             case <-time.After(time.Second * 3):
                fmt.Println("已超时3秒")
                done <- true
             }
          }
       }()
    
       for i := 0; i < 10; i++ {
          ch <- i
       }
    
       <-done
       fmt.Println("程序终止")
    }
    
    • 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
    • 28
    • 29
    • 30

    在这里插入图片描述

    13.4.3 死锁

    在编写并发程序时可能会碰到死锁。什么是死锁?就是所有的线程或进程都在等待资源的释放。

    package main
    
    func main() {
        ch := make(chan int)
        <- ch // 阻塞main goroutine, 信道ch被锁
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    程序中只有一个协程,向里面加入数据或者存数据的话,都会锁死信道,并且阻塞当前协程。也就是所有的goroutine(其实只有main线程一个)都在等待信道的开放(没人拿走数据的话,信道是不会开放的),这就产生了死锁。

    在非缓冲信道上如果发生了流入无流出,或者流出无流入,都会导致死锁。同样地,使用select关键字,其中不加任何代码也会产生死锁。

    package main
    
    func main() {
       select {}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    在这里插入图片描述

  • 相关阅读:
    【不存在的人】用Python获取生成随机头像,还不侵权
    Java NIO Selector 的使用
    艾美捷PEG-2000 DMG解决方案
    线上问题:Harbor核心服务不可用
    nlp之文本转向量
    阿尔萨斯监控平台&普罗米修斯监控平台对服务器资源的监控
    vue监听表单输入的身份证号自动填充性别和生日
    [Linux]如何配置定期任務腳本,並將執行結果寄到外部郵箱? (下)
    pycharm安装opencv-python报错
    linux:查看文件前100行和后100行
  • 原文地址:https://blog.csdn.net/weixin_44226181/article/details/125910807