go语言之并发-信道深度理解

------------------------------信道是协程之间通信的管道,从一端发送数据,另一端接收数据----------------------------

创建信道

var c chan int    // 方式一
c := make(chan int)  // 方式二
使用关键字 chan 创建信道,声明时有类型,表明信道只允许该类型的数据传输。信道的零值为 nil

方式一:就声明了 nil 信道。nil 信道没什么作用,既不能发送数据也不能接受数据。
方式二:使用 make 函数创建了可用的信道 c。

func main() {

 c := make(chan int)
 fmt.Printf("Type is %T\n",c)
 fmt.Printf("Value is %v\n",c)
}
//输出:
Type is chan int
Value is 0xc000060060

创建了信道 c,而且只允许 int 型数据传输。一般将信道作为参数传递给函数或者方法实现两个协程之间通信,信道 c 的值是一个地址,传参的时候直接使用 c 的值就可以,而不用取址。

信道的使用

1.读写数据
c := make(chan int)
// 写数据
c <- data   
// 读数据
variable <- c  // 方式一
<- c   // 方式二,表示读出来的数据丢弃

重点注意
信道操作默认是阻塞的,往信道里写数据之后当前协程便阻塞,直到其他协程将数据读出。一个协程被信道操作阻塞后,Go 调度器会去调用其他可用的协程,这样程序就不会一直阻塞

func printHello(c chan bool) {
   fmt.Println("hello world goroutine")
   <- c    // 读取信道的数据
}

func main() {
   c := make(chan bool)
   go printHello(c)
   c <- true    // main 协程阻塞
   fmt.Println("main goroutine")
}
//输出:
hello world goroutine
main goroutine

main协程创建peintHello协程之后,往c中写数据,main协程开始阻塞,
go调度器调度printHello协程从信道读取数据,main协程解除阻塞继续运行。
读取操作没有阻塞是因为信道C已有可读的数据,否则读取操作会阻塞。

死锁:

读/写数据的时候信道会阻塞,调度器会去调度其他可用的协程。如果没有其他可用的协程会发生什么情况?没错,就会发生著名的死锁。常见情况的情况就是,只往信道写数据

func main() {

   c := make(chan bool)
   c <- true    // 只写不读
   fmt.Println("main goroutine")
}
//输出:error
fatal error: all goroutines are asleep - deadlock!

同样的,只读不写(通道中没数据)会报同样的错误!

通道的关闭及状态检测:

发送数据的信道有能力选择关闭信道,数据就不能传输。数据接收的时候可以返回一个状态判断该信道是否关闭:

val, ok := <- channel

val 是接收的值,ok 标识信道是否关闭。为 true 的话,该信道还可以进行读写操作;
为 false 则标识信道关闭,数据不能传输。

使用内置函数 close() 关闭信道

func printTest(ch chan int) {
   for i := 0; i < 4; i++ {
     ch <- i
   }
   close(ch)
}

func main() {
   ch := make(chan int)
   go printTest(ch)
   
   for {
      v, ok := <-ch
      if false == ok {     // 通过 ok 判断信道是否关闭
        fmt.Println(v, ok)
        break
      }
     fmt.Println(v, ok)
  } 
 
}
//输出:
0 true
1 true
2 true
3 true
0 false

使用 for 循环,需要手动判断信道有没有关闭。也可以使用 for range 读取信道,信道关闭,for range 自动退出。发送完毕必须使用close()信道,不然会发生死锁。

func printTest(ch chan int) {
   for i := 0; i < 4; i++ {
     ch <- i
   }
   close(ch)
}

func main() {
   ch := make(chan int)
   go printTest(ch)
   for v := range ch {
     fmt.Println(v)
   }
}
//输出:
0
1
2
3

缓冲信道和信道容量:

上面创建的信道是无缓冲的,读写信道会立马阻塞当前协程。对于缓冲信道,写不会阻塞当前信道直到信道满了,同理,读操作也不会阻塞当前信道除非信道没数据。创建带缓冲的信道:

ch := make(chan type, capacity)  
capacity 是缓冲大小,必须大于 0。 内置函数 len()cap() 可以计算信道的长度和容量
func main() {
   ch := make(chan int,3)
   ch <- 7
   ch <- 8
   ch <- 9
   //ch <- 10    
   // 注释打开的话,协程阻塞,发生死锁
  // 会发生死锁:信道已满且没有其他可用信道读取数据
   fmt.Println("main stop")
}
//输出:
main stop

创建了缓冲为 3 的信道,写入 3 个数据时信道不会阻塞。注释打开的话,此时信道已满,协程阻塞,又没有其他可用协程读数据,便发生死锁。

func printNums(ch chan int) {
   ch <- 7
   ch <- 8
   ch <- 9
   fmt.Printf("channel len:%d,capacity:%d\n",len(ch),cap(ch))
   fmt.Println("blocking...")
   ch <- 10   // 阻塞
   close(ch)
}

func main() {

   ch := make(chan int,3)
   go printNums(ch)
   // 休眠 2s
   time.Sleep(2*time.Second)
   for v := range ch {
     fmt.Println(v)
   }
   fmt.Println("main stopped")
}
//输出';
channel len:3,capacity:3
blocking...
7
8
9
10
main stopped
2s 之后,主协程从信道读取数据,信道容量有余阻塞便解除,继续写数据

如果缓冲信道是关闭状态但有数据,仍然可以读取数据:

func main() {

   ch := make(chan int,3)
   ch <- 7
   ch <- 8
   //ch <- 9
   close(ch)
 
   for v := range ch {
     fmt.Println(v)
   }
  fmt.Println("main stop")
}
//输出:
7
8
main stopped

单向通道:

之前创建的都是双向信道,既能发送数据也能接收数据。我们还可以创建单向信道,只发送或者只接收数据。

sendch := make(<-chan int)     
recvch := make(chan<- int)
sendch 是只发送信道,recvch 是只接受信道。

这种单向信道有什么用呢?我们总不能只发不接或只接不发吧。这种信道主要用在信道作为参数传递的时候,Go 提供了自动转化,双向转单向。

func printTest(ch chan<- int) {

   for i := 0; i < 4; i++ {
       ch <- i
    }
   close(ch)
  
}

func main() {

   ch := make(chan int)
   go printTest(ch)
   for v := range ch {
      fmt.Println(v)
   }
 
}
//输出:
0
1
2
3

main 协程中 ch 是一个双向信道,printTest() 在接收参数的时候将 ch 自动转成了单向信道,只发不收。
但在 main 协程中,ch 仍然可以接收数据。使用单向通道主要是可以提高程序的类型安全性,程序不容易出错。

信道数据类型:

信道是一类值,类似于 int、string 等,可以像其他值一样在任何地方使用,比如作为结构体成员、函数参数、函数返回值,甚至作为另一个通道的类型。我们来看下使用通道作为另一个通道的数据类型

func printWord(ch chan string) {
 fmt.Println("Hello " + <-ch)
}

func productCh(ch chan chan string)  {
 c := make(chan string)   // 创建 string type 信道
 ch <- c     // 传输信道
}

func main() {

 // 创建 chan string 类型的信道
 ch := make(chan chan string)
 go productCh(ch)
 // c 是 string type 的信道
 c := <-ch
 go printWord(c)
 c <- "world"
 fmt.Println("main stopped")
}
//输出:
Hello world
main stopped
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值