一文彻底搞懂GO中的管道(Channel)操作!

相信学过GO的人,对协程不陌生,但管道(通道)中有很多细节可能是你们不清楚的,比如管道关闭后,里面是否还有数据?为什么要管道关闭后再遍历?管道带不带缓冲区有什么区别?管道的同步和异步操作又是怎么回事?select对管道的影响又是什么?别急,本文带你一次性搞懂管道!

一、基本概念

channel,通道,又译作管道,是GO中的重要数据类型,用于传递数据。通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。
通道必须使用 make 函数创建,有三种创建方式:

ch := make(chan int) //双向通道
ch := make(<-chan int) //只读通道
ch := make(chan <-int) //只写通道

其中<-为取出(读)或放入(写)操作,操作符在chan左边为读,在chan右边为写。

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:

ch := make(chan int, 100)

二、无缓冲通道的作用

如果通道不带缓冲,就是同步操作,即发送方会阻塞直到接收方从通道中接收了值,这可以用于使主线程等待协程完成通信。为了验证这一点,我们写两个函数,一个用来跑 goroutine,另一个作为主线程,并且两个都带Sleep,看看会发生什么:

package main
import (
    "fmt"
    "time"
)
func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s, (i+1)*100)
    }
}
func say2(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println(s, (i+1)*150)
    }
}
func main() {
    go say2("world")
    say("hello")
}

输出结果:

hello 100
world 150
hello 200
hello 300
world 300
hello 400
world 450
hello 500

问题来了,say2 只执行了 3 次,而不是设想的 5 次,为什么呢?原来,在 goroutine 还没来得及跑完 5 次的时候,主函数已经退出了。我们要想办法阻止主函数的结束,要等待 goroutine 执行完成之后,再退出主函数:

package main

import (
	"fmt"
	"time"
)

func say(s string) {
	for i := 0; i < 5; i++ {
		time.Sleep(100 * time.Millisecond)
		fmt.Println(s, (i+1)*100)
	}
}
func say2(s string, ch chan int) {
	for i := 0; i < 5; i++ {
		time.Sleep(150 * time.Millisecond)
		fmt.Println(s, (i+1)*150)
	}
	ch <- 0
	close(ch)
}

func main() {
	ch := make(chan int)
	go say2("world", ch)
	say("hello")
	fmt.Println(<-ch) //关键语句,这句会阻塞,直到say2执行完毕
}

我们引入一个通道,默认的,通道的存消息和取消息都是阻塞的,在 goroutine 中执行完成后给通道一个值 0,则主函数会一直等待通道中的值,一旦通道有值,主函数才会结束

三、通道的同步与异步

通道缓冲的形象解释:
无缓冲是同步的,例如 make(chan int),就是一个送信人去你家门口送信,你不在家他不走,你一定要接下信,他才会走,无缓冲保证信能到你手上。
有缓冲是异步的,例如 make(chan int, 1),就是一个送信人去你家仍到你家的信箱,转身就走,除非你的信箱满了,他必须等信箱空下来,有缓冲的保证信能进你家的邮箱。
接下来演示异步通道:

package main

import (
	"fmt"
	"time"
)

func put(c chan int) {
	for i := 0; i < 10; i++ {
		c <- i
		time.Sleep(100 * time.Millisecond)
		fmt.Println("->放入", i)
	}
	fmt.Println("=所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。")
	close(c)
}
func main() {
	ch := make(chan int, 5)
	go put(ch)
	for {
		time.Sleep(1000 * time.Millisecond)
		data, ok := <-ch
		if ok {
			fmt.Println("<-取出", data)
		} else {
			break
		}
	}
}

输出结果:

->放入 0
->放入 1
->放入 2
->放入 3
->放入 4
<-取出 0
->放入 5
<-取出 1
->放入 6
<-取出 2
->放入 7
<-取出 3
->放入 8
<-取出 4
->放入 9
=所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。
<-取出 5
<-取出 6
<-取出 7
<-取出 8
<-取出 9

可以看到,放的速度较快,先放满了 4 个,阻塞住。取的速度较慢,放了4个才开始取,由于缓冲区已经满了,所以取出一个之后,才能再次放入。放完了之后虽然缓冲区关闭了,但是缓冲区的内容还保留,所以还能继续取出。

四、通道的遍历与协程死锁

关闭通道并不会丢失里面的数据,只是防止通道数据被读完后一直阻塞,即等待新数据写入。所以遍历通道前必须关闭,关闭后的管道,读完数据才不会阻塞,实例如下:

package main

import (
	"fmt"
)

func fibonacci(n int, c chan int) {
	x, y := 0, 1
	for i := 0; i < n; i++ {
		c <- x
		x, y = y, x+y
	}
	close(c)
}

func main() {
	c := make(chan int, 10)
	go fibonacci(cap(c), c)
	// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个
	// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据
	// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不
	// 会结束,从而在接收第 11 个数据的时候就阻塞了。
	for i := range c {
		fmt.Println(i)
	}
}

输出结果:

0
1
1
2
3
5
8
13
21
34

如果把close那句注释,则结果会是这样:

0
1
1
2
3
5
8
13
21
34
fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()
        d:/code/golang/02并发编程/04travel/travel.go:23 +0xdb
exit status 2

其中all goroutines are asleep - deadlock!这句说明管道阻塞,原因前面说了,管道未关闭,数据被读完后会一直等待新数据进入,就发生了管道阻塞,而此时所有协程会相互等待,由于程序中没有管道写入操作了,所以协程之间会无限地等待下去,这样就发生了死锁(deadlock)!

五、select的作用

select 语句使得一个 goroutine 可以等待多个通信操作。select中的每个case必须是一个通道操作。select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行:

package main

import "fmt"

func fibonacci(c, quit chan int) {
	x, y := 0, 1
	for {
		select {
		case c <- x:
			x, y = y, x+y
		case <-quit:
			fmt.Println("quit")
			return
		}
	}
}

func main() {
	c := make(chan int)
	quit := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			fmt.Println(<-c)
		}
		quit <- 0
	}()
	fibonacci(c, quit)
}

输出结果:

0
1
1
2
3
5
8
13
21
34
quit

显然,case <-quit这句是在所有case c <- x执行完后再执行的。原因不难分析:quit通道在协程中的for循环结束前是空的,取出操作会造成通道阻塞,所以对于select来说,quit是一直未准备好的。而前面说过,无缓冲通道无论进行存还是取都会阻塞,但这里的协程中,每次循环时,c都会有取出操作,所以c的读和写能一直进行,即第一个case会一直是准备(可执行)状态,直到循环结束。循环结束后,c不再有取出操作,所以第一个case就会阻塞(注意不再有写入操作也行,无缓冲的阻塞是双向的),变成未准备状态,而quit <- 0又刚好使第二个case变成准备状态,所以程序输出34后一定会执行第二个case,输出quit。主线程输出完毕后return,主线程结束,那么整个程序也跟着结束。

相信如果你认真读到这里,一定会对通道的认识更深,甚至有了一个新的高度。你不理解管道,是因为有很多细节没被你注意到,如果你注意到并总结了这些细节,你会发现其实管道也就这点东西。

Python面向对象编程(Object-Oriented Programming,简称OOP)是一种编程范式,它将数据和操作封装在对象,通过对象之间的交互实现程序的设计和开发。下面是一些关键概念,帮助你更好地理解Python面向对象编程。 1. 类(Class):类是对象的蓝图或模板,描述了对象的属性和行为。它定义了对象的特征和方法。例如,我们可以定义一个名为"Car"的类来表示汽车,其包含属性(如颜色、型号)和方法(如加速、刹车)。 2. 对象(Object):对象是类的实例,是具体的实体。通过实例化类,我们可以创建一个对象。例如,我们可以创建一个名为"my_car"的对象,它是基于"Car"类的实例。 3. 属性(Attribute):属性是对象的特征,用于描述对象的状态。每个对象都可以具有一组属性。例如,"Car"类的属性可以包括颜色、型号等。 4. 方法(Method):方法是对象的行为,用于定义对象的操作。每个对象都可以具有一组方法。例如,"Car"类的方法可以包括加速、刹车等。 5. 继承(Inheritance):继承是一种机制,允许我们创建一个新类(称为子类),从现有类(称为父类)继承属性和方法。子类可以扩展或修改父类的功能。继承可以实现代码重用和层次化设计。 6. 多态(Polymorphism):多态是一种特性,允许不同类的对象对同一方法做出不同的响应。多态提高了代码的灵活性和可扩展性。 7. 封装(Encapsulation):封装是一种将数据和操作封装在对象的机制,隐藏了对象的内部实现细节,只暴露必要的接口给外部使用。这样可以保护数据的安全性,提供了更好的模块化和代码复用性。 通过理解这些概念,你可以更好地掌握Python面向对象编程。在实践,你可以使用类来创建对象,操作对象的属性和调用对象的方法,通过继承和多态实现代码的灵活性和可扩展性,通过封装保护数据的安全性和提高代码的可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

技术卷

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值