10 | 通道的基本操作
10 | 通道的基本操作
讲述:黄洲君
时长12:42大小8.71M
前导内容:通道的基础知识
问题解析
知识扩展
总结
思考题
赞 40
提建议
精选留言(75)
- 忘怀2018-09-12Go里没有深copy。 即便有的话这里可能也不会用吧,创建一个指针的内存开销绝大多数情况下要比重新开辟一块内存再把数据复制过来好的多吧。 老师,这么说对吗?
作者回复: 对,这就是传指针值的好处之一。
56 - colonel2018-09-23通道底层存储数据的是链表还是数组?
作者回复: 环形链表
共 2 条评论42 - 江山如画2018-09-03老师回复我后突然感觉不对劲,结构体是值类型,通道传输的时候会新拷贝一份对象,底层数据结构会被复制,引用类型可能就不一定了,又用数组和切片试了下,发现切片在通道传输的时候底层数据结构不会被复制,改了一个另外一个也会跟着改变,所以切片这里应该是浅复制,数组一个改了对另一个没有影响是深层复制,代码: // ch := make(chan []int, 1) s1 := []int{1, 2, 3} ch <- s1 s2 := <-ch s2[0] = 100 fmt.Println(s1, s2) //[100 2 3] [100 2 3] // ch2 := make(chan [3]int, 1) s3 := [3]int{1, 2, 3} ch2 <- s3 s4 := <-ch2 s3[0] = 100 fmt.Println(s3, s4) //[100 2 3] [1 2 3]展开
作者回复: 再说一遍,Go语言里没有深层复制。数组是值类型,所以会被完全复制。
共 12 条评论35 - melon2018-09-03感觉chanel有点像socket的同步阻塞模式,只不过channel的发送端和接收端共享一个缓冲,套接字则是发送这边有发送缓冲,接收这边有接收缓冲,而且socket接收端如果先close的话,发送端再发送数据的也会引发panic(linux上会触发SIG_PIPE信号,不处理程序就崩溃了)。 另使用demo21.go测试发送接收阻塞情况时需要额外空跑一个goroutine,否则会引发这样的panic(至 少1.11版是这样):fatal error: all goroutines are asleep - deadlock!展开
作者回复: 对,所以注释中才会那么说。
27 - 来碗绿豆汤2018-09-03深copy还是浅copy,跟具体数据类型有关,引用型数据就是浅copy,数值型数据就是深copy.如,如果是切片类型则是浅copy,如果是数组则是深copy
作者回复: 其实都是浅表复制。数组因为是值类型的,所以即使是浅复制也能完全复制过来。
共 2 条评论19 - 会哭的鱼2019-03-03老师您好,通道这里我看了好几遍了,对于评论中有一个问题一直不明白,非常希望老师能够解答一下! 同学阿拉丁的瓜的提问: 请问老师,缓冲通道内的值是被并行读出的吗? 比如两个goroutine分别为r1和r2;一个装满的容量为2的chan。 当r1正在取出先入的数据时,r2是否可以取出后入的数据;还是说r2必须阻塞,等到先入数据被完全取走之后才能开始读取后入的数据? 老师回答: 作者回复: 可以同时进行,通道是并发安全的。但是不一定哪个g拿到哪个元素值。 个人不明白,按照我看完的理解,同一个通道不管有多少并发在接收操作,同一个通道同时只能被一个goroutine操作,其他的都要在这个接收操作完成 “复制通道内的元素值”“放置副本到接收方”“删掉原值”三步完全完成后才可以继续进行的,负责就要一直阻塞才对 老师原文中是这样的: 类似的,在同一时刻,运行时系统也只会执行,对同一个通道的任意个接收操作中的某一个。 直到这个元素值完全被移出该通道之后,其他针对该通道的接收操作才可能被执行。即使这些操作是并发执行的也是如此。 这里所谓的并发执行,你可以这样认为,多个代码块分别在不同的 goroutine 之中,并有机会在同一个时间段内被执行。 请老师解答一下我这个疑惑,非常感谢!展开
作者回复: 你理解的没错,在同一时刻,只有一个goroutine能够对某一个通道进行取出操作,其他的试图对这个通道进行取出操作的goroutine都会被阻塞,并进入通道内部的队列排队。通道会保证这种操作是互斥的,并且是原子性的(完全取走一个元素值之后,下一个元素值才有可能被取)。 我回答那位同学的意思是:两个go函数中的代码是有可能同时(在同一个很小的时间段内)执行到“取出操作”那一行代码的。不过我们完全不用在意,因为通道和运行时系统会保证这类操作的并发安全。 可能我那个回答太短了吧,咱俩没有对上口径。
共 4 条评论15 - 有铭2018-09-03通道看上去像线程安全队列。那么这玩意在低层基于什么原理实现?cas?自旋?内核锁?性能如何13
- My dream2018-09-21老师,请教一下,通道的传值可以直接传指针不,你讲的拷贝,那么内存开销是很大的,如果通道传指针的话,会不会好很多
作者回复: 原则上可以传任何类型的数据。不过,要是传指针的话要自己保证安全啊,原始数据放篡改之类的。
12 - 请叫我小岳同学2018-09-031. 通道的长度,表示channel 缓冲的长度。当channel处于阻塞状态时,容纳最多的同类型的长度。 2. 深拷贝
作者回复: 第一个问题,长度代表通道当前包含的元素个数,容量就是初始化时你设置的那个数。 第二个问题你再想想,可以做做试验。
11 - Geek_a8be592019-06-27ch1 := make(chan int, 2) // 发送方。 go func() { for i := 0; i < 10; i++ { fmt.Printf("Sender: sending element %v...\n", i) ch1 <- i } fmt.Println("Sender: close the channel...") close(ch1) }() // 接收方。 for { elem, ok := <-ch1 if !ok { fmt.Println("Receiver: closed channel") break } fmt.Printf("Receiver: received an element: %v\n", elem) } fmt.Println("End.") 老师,根据您的提供的源码有三个问题需请教。 第一问题:第一次携程调度应该发生主携程中的elem, ok := <-ch1 这个代码处,这时候应该在chan有等待的协成,再第一向chan1<-i传值得时候,根据您的描述"当发送操作在执行的时候发现空的通道中,正好有等待的接收,那么会把元素直接复制给对方"。照这么说应该在这次就跳转到主协成中,并打印出接收到的数据了。但是实际是先发送i=3的时候才做第一次携程调度,请问这是为什么? 第二问题:缓存区的大小不是设置的是2么,为什么length当发送了i=3的时候才会阻塞发生调度呢,正常不是应该i=2的时候么 第三个问题:当for循环结束了以后 就是在chan关闭之前,为什么又能调度到主协成让他接收呢。不应该到这个协成调用结束么?展开
作者回复: 所以说不要用“协程”这个概念,因为“协程(coroutine)”指的是程序在同一个线程内的自行调度,是应用程序本身完全可控的。而 goroutine 的调度是 Go 语言的运行时系统发起的。 你不要揣测 Go 语言的调度器会怎样调度。你首先要知道哪些代码点是调度的时机(注意,到了调度时机也不一定发生调度,只是时机而已)。你还要知道如果想让多个 goroutine 按照你拟定的流程执行就需要用到 Channel 以及各种同步工具。 你说的“跳转到”只能在 coroutine 场景下才能这么说。在 goroutine 的场景下,没有“跳转”这么一说。 其一,你在上面的 for 语句中启用了一个 goroutine,你怎么就能断定后面的代码一定会先于这个 go 函数执行?不要做这种假设。因为连 goroutine 的调度都是并发的。 其二,两个 goroutine 一个 channel,一个 goroutine 发,一个 goroutine 取。这个 ch1 什么时候满、什么时候空,你基本上是确定不了的。因为两个 for 循环 在迭代的过程中都可能因被调用而换下 CPU。 其三,你要知道,几乎任何函数调用都存在调度时机,更何况是像 fmt.Println 这种需要 I/O 的重型操作。所以,为什么你那前一个 for 循环结束之后就不能被调度了呢? 以上是我通过你的文字表达猜测并回答的,并不一定完全匹配你要问的问题。还有问题的话再问我。 我觉得你对“并发”和“调度”这两个概念不清楚。我建议你好好看看专栏里讲 goroutine 的那几篇文章。有必要的话,买我的《Go 并发编程实战》第二版从头学一下。
共 3 条评论10 - Yayu2018-09-05老师,我知道 golang 这门语言中所有的变量赋值操作都是 value copy的,不论这个变量是值类型,还是指针类型。关于您这里说的 shallow copy 与 deep copy 的问题我还是不是很清楚, google 了一下,每门语言的支持都不太一样,您是怎么定义这两个概念的?能否详细说一下?
作者回复: 浅拷贝只是拷贝值以及值中直接包含的东西,深拷贝就是把所有深层次的结构一并拷贝。
10 - 苏浅2018-09-03通道必须要手动关闭吗?go会自动清理吗?
作者回复: 需要手动关闭,这是个很好的习惯,而且也可以利用关的动作来给接收方传递一个信号。Go的GC只会清理被分配到堆上的、不再有任何引用的对象。
10 - kk2019-12-26技术课却没几行代码,全是大段的文字,读起来晦涩佶屈共 2 条评论7
- Xiaolan🇨🇳2019-03-12通道的使用场景是不是同一个进程的不同线程间通讯使用?如果是不同程序进程还可以使用吗?
作者回复: 它是面向同一个进程的。多个进程之间内存一般不会共享,所以没法用channel。进程间通讯可以考虑IPC方法,比如有名管道,你可以参看一下os.Pipe函数的文档。其实进程间通讯最强大和灵活的还是socket。
6 - Geek_a8be592019-06-28对昨天您提的问题做了一下补充: 1. 你的 Wait 方法里的那个 for 语句是干嘛的? 2. total :=<-s.nowtotal 中的 total 不是一个会改变的数啊,那你后边的 total == s.now-1 判断就存在问题。 ------------------------------ // 抽象一个栅栏 type Barrier interface { Wait () } // 创建栅栏对象 func NewBarrier (n int) Barrier { var barrier = &barrier{} barrier.now = n barrier.chanArr = make(chan int) barrier.nowtotal = make(chan int) go barrier.NowTotal() return barrier } func (s *barrier) NowTotal() { //用于判断当前属于第几个运行的goroutine for i:=0;i<s.now;i++ { s.nowtotal<-i } } func (s *barrier) Wait() { total :=<-s.nowtotal if total == s.now-1 { //这里s.now-1等于9就是说明这是第10个goroutine for i:=0;i<total;i++ { //这个是为了去唤醒剩余的9个。注:题目的要求就是前9个goroutine阻塞,第10个goroutine去唤醒他们 s.chanArr<-i } } else { num:=<-s.chanArr fmt.Printf("拿到了数据:%v\n",num) } } // 栅栏的实现类 type barrier struct { chanArr chan int nowtotal chan int now int } // 测试代码 func main () { // 创建栅栏对象 b := NewBarrier(10) // 达到的效果:前9个协程调用Wait()阻塞,第10个调用后10个协程全部唤醒 for i:=0; i<10; i++ { go b.Wait() } select { } } 第一个您的问题:for循环主要去唤醒其他阻塞的goroutine 第二个您的问题:s.nowtotal 一直是在变得呀,通过传i进来,主要说明当前是运行的第几个goroutine 只是整体用的语意不这么明确 这样您看我的能实现要求么?展开
作者回复: 为什么非要知道那些 goroutine 都是第几个呢?再说了,你那个 NowTotal 中的 for i:=0;i<s.now;i++ { s.nowtotal<-i } 相当于向 nowtotal 发送 0、1、2、3、4、5、6、7、8、9。那你的 Wait 方法每次给 total 赋的值就是不一样的。即使你的 if total == s.now-1 可以判断已经够 10 个 goroutine了,可 s.chanArr<-i 不应该是最后一个 Wait 方法做的事。你想想,你这样只能完成一次“阻塞 9 个,满 10 个放行”。 我觉得你实现得不好。职责划分不清晰。你应该将把控的逻辑放在 NowTotal 方法里。Wait 方法每执行一次就通过通道告诉 NowTotal 一下。告诉完了,Wait 方法用另一个通道阻塞自己。一旦够了 10 个,NowTotal 就通过第二个通道放行。
4 - 勇.Max2018-11-15老师,有个问题困惑很久,如果传指针的话,接收方和发送方不在一台机器上,指针还有效吗?(指针不是指向本地内存的吗)共 3 条评论4
- My dream2018-09-27通道传值首先要保证原始数据的安全性是吗?所以一般不建议用传指针的方式来通讯,是不是这样理解的
作者回复: 如果你的通道要给外人使用,或者通过通道对外提供功能,那就不要传指针值了,容易造成安全漏洞,另外这个时候最好限制下通道的方向。
5 - wh2018-09-05不要从接受端关闭channel算是基本原则了,另外如果有多个并发发送者,1个或多个接收者,有什么普适选择可以分享吗?
作者回复: 可以用另外的标志位做,比如context。
4 - 皮卡丘2018-09-03很明显现象是浅拷贝,为啥那么多说深拷贝的4
- 🐶2019-08-10通道的长度代表着整个通道已经存在的元素值len可以查看,类似于切片,而容量则是刚开始设定的值,整个chan可以看成一个已经长度的队列,操作也可以跟队列相类比。 go没有深拷贝,只有浅拷贝,通过值拷贝,而没有所谓的引用拷贝,也是因为这样,整个go语言才能使得效率很高,内存占用少! 建议:老师可以画一些图来理清思路,因为一直看文字会很累...
作者回复: 好,收到。
3