12 | 进程内协同:同步、互斥与通讯
12 | 进程内协同:同步、互斥与通讯
讲述:丁伟
时长15:40大小14.39M
原子操作
执行体的互斥
执行体的同步
执行体的通讯
结语
赞 25
提建议
精选留言(65)
- 笨拙的自由2019-05-24希望老师类比java说一下,特别是条件原语那块看得有点懵
作者回复: 如果用 Java,代码看起来是这样的: class Channel { private final Lock lock = new ReentrantLock(); private Condition cond = lock.newCondition(); private final Queue queue = new Queue(); private int n; public Channel(int cap) { n = cap; } public void push(Object v) { lock.lock(); try { while (queue.size() == n) { cond.await(); } if (queue.size() == 0) { cond.signalAll(); } queue.push(v); } finally { lock.unlock(); } } public Object pop() { lock.lock(); try { while (queue.size() == 0) { cond.await(); } if (queue.size() == n) { cond.signalAll(); } return queue.pop(); } finally { lock.unlock(); } } }
共 3 条评论51 - 立耳2019-05-27许老师,下面的这段代码是不是存在问题,以Push为例,应该先执行 c.queue.Push 再进行广播,否则可能通知到其他协程进行Pop,系统调度可能先进行了另外一个协程的c.queue.Pop(), 这个时候还没有入队列。 func (c *Channel) Push(v interface{}) { c.mutex.Lock() defer c.mutex.Unlock() for c.queue.Len() == c.n { // 等待队列不满 c.cond.Wait() } if c.queue.Len() == 0 { // 原来队列是空的,可能有人等待数据,通知它们 c.cond.Broadcast() } c.queue.Push(v) } func (c *Channel) Pop() (v interface{}) { c.mutex.Lock() defer c.mutex.Unlock() for c.queue.Len() == 0 { // 等待队列不空 c.cond.Wait() } if c.queue.Len() == c.n { // 原来队列是满的,可能有人等着写数据,通知它们 c.cond.Broadcast() } return c.queue.Pop() } func (c *Channel) TryPop() (v interface{}, ok bool) { c.mutex.Lock() defer c.mutex.Unlock() if c.queue.Len() == 0 { // 如果队列为空,直接返回 return } if c.queue.Len() == c.n { // 原来队列是满的,可能有人等着写数据,通知它们 c.cond.Broadcast() } return c.queue.Pop(), true } func (c *Channel) TryPush(v interface{}) (ok bool) { c.mutex.Lock() defer c.mutex.Unlock() if c.queue.Len() == c.n { // 如果队列满,直接返回 return } if c.queue.Len() == 0 { // 原来队列是空的,可能有人等待数据,通知它们 c.cond.Broadcast() } c.queue.Push(v) return true }展开
作者回复: 代码没有问题的。先 Push 还是先通知都可以,次序可以交换的。因为反正锁还没有释放,这里只是标记一下哪些执行体可以调度,并没有真正发生控制权的转移。而且就算转移了也没问题的,你可以留意下本文贴的条件变量的 Wait 函数实现,它获得控制权后下一句就是 mutex.Lock 去申请锁,而我们这里是 Push 后才调用 mutex.Unlock 释放锁的,所以 Broadcast 和 Push 的次序可以随意交换。
共 4 条评论21 - 蚂蚁内推+v2019-05-24协程和线程还是没区别清楚
作者回复: 一个操作系统调度,一个用户态自己来调度
共 4 条评论15 - Geek_gooy2019-05-26老师 我明白了, 比如A线程notify或者signal,被唤醒的线程并不会马上执行,而是需要等待A线程退出同步块或者unlock才会执行。 如果是notifyAll,也同样如此,但是等到唤醒并获得执行权的线程执行结束后,CPU会优先把执行权交给上次唤醒没有得到执行权的某个线程,而不会给阻塞在锁外面等待锁的线程。和调用notify只唤醒一个还是有些许区别的。展开
作者回复: 是这样,你可以看我文章中Wait的代码,在唤醒后第一件事情是lock,也就是请求锁,所以只有A线程unlock后,其他被唤醒的线程中的一个会得到锁往下走。
12 - Geek_gooy2019-05-25老师 1、像这种有进有出的是不是应该创建两个condition。大小为满时,避免进的线程,唤醒的可能还是进的线程。大小为零时,出的唤醒的还是出的线程。 2、cond.signal()方法把lock锁释放了吗,如果释放了,后面再unlock是不是没做任何操作。 3、像老师评论中的Java单锁channel举例改为普通的sync,object.wait(),notify是不是效果一样,但性能没lock好。对于两个condition,java对象的notify就不好指定唤醒了。展开
作者回复: 1、你是对的,用两个cond性能会更好。但是用一个也是可以正常工作的。 2、cond.signal 不是把锁释放了,是让等待在这个cond上的执行体改变状态(从挂起到可被调度),从而允许调度程序给它执行权。 3、对的
11 - f抵达2019-05-26如果是用户太自己对寄存器进行操作? 对物理器件的操作不都是要经过系统调用么? 难道协程x是用户态的操作系统?
作者回复: 寄存器不是输入输出设备,操作寄存器不需要经过操作系统,编译器整天和寄存器打交道的。
8 - Taozi2019-05-25总算明白为什么叫条件变量了,拿这里的Channel 实现来说,几个执行体要读写的队列是“变量”,队列的长度是“等待条件”和“唤醒条件”。是这样理解吗?
作者回复: 对的
8 - Cordova2019-05-28看完代码后发现go做的通信发现并没有什么优势,其他语言做通信也这么干、刷了下评论区提到libevent、才恍然发现标题这节讲的是同步!可能是我太期待许老师讲异步了😂~ 目前python我用异步首选会把异步过程交给libuv。听了老师上节讲到python的协程只是一种编程范式,想到内置的asyncio虽然是做了异步但还是有很较大的性能提升空间这个逻辑也就通了!希望许老师在讲异步的时候能多提一提跨平台异步库他们是怎么实现
作者回复: 1、go和java的代码只是形似,实质不同,因为go里面的channel是协程的通讯设施,java版本的是线程的通讯设施,大相径庭; 2、我们本节提的同步,和同步io的同步,两个是完全不同含义的同步; 3、我们课程不太会讲资料已经相对多的某个细节,除非这个细节非常关键影响到全局的理解。
共 2 条评论7 - Eternal2020-09-11信号量只能实现一个条件,条件变量能实现多了条件,不知理解对不对
作者回复: 信号量的条件太死板,不可编程。条件变量的条件是任意条件,是可编程的。
5 - Ender2019-06-06还是没太明白条件变量在channel代码里面的意义,所有操作都是先获取锁,在一个操作没完成的情况下其他都不会进到cond.Wait()呀。按理只需要锁就能做到了channel的实现了。
作者回复: 向 channel.push 一个对象时,要考虑 channel 满了,这时会等待,这就是 cond.Wait 的逻辑
4 - 輪迴2019-05-24同时在看《深入浅出计算机组成原理》和《GO 语言从入门到实战》,发现三个课程之间的关联性还是蛮多的,相辅相成,更加帮助知识点的深入理解
作者回复: 👍
4 - thewangzl2019-05-29老师你好,文章中说Signal比Broadcast好些,但是王宝令老师的专栏《Java并发编程实战》第15章提到Dubbo唤醒等待线程从signal优化为signalAll。是因为java中的signal/signalAll和Go的Signal/Broadcast有差异吗?
作者回复: 你说的是对的。我其实没想到什么情况下用signal,大部分情况下都是用 broadcast,包括本文中的例子。因为用 signal 意味着每次资源的使用都要通知,其实退化为信号量的 PV 操作了,这一定性能是变差的。
4 - Geek_03056e2019-05-26将线程统一申请,弄一个线程池,使用条件变量唤醒,使用时唤醒,用完后休眠,就像nginx中的线程池。这种设计是不和协程就一样了,在用户态构建执行体?
作者回复: 不一样,这样写最终用的是libevent的异步回调模式。没法用同步io模型。
3 - 白小狮2019-05-25go中的panic会导致整个主进程都挂掉,goroutine里面的panic后cover不住,是逻辑上就是应该整个主进程都退出吗
作者回复: 设计上,goroutine 应该自己去 recover 错误,而不是主进程来 recover。
3 - cc2019-05-25老师,我看你的chanel代码实现,比如pop方法。是先broadcast,后pop。我的理解刚好相反。应该是先pop后通知。请教下问什么
作者回复: 次序可以交换的。因为反正锁还没有释放,这里只是标记一下哪些执行体可以调度
3 - mz2019-05-24感觉和 Java 的 wait notify notifyall 很像共 1 条评论3
- Bachue Zhou2019-05-28看不懂为什么要把管道的使用场景放在 Goroutine 之间通讯,这并不是管道合理的使用场景啊,至于和 Channel 做对比就更没有意义了。
作者回复: 管道合理的使用场景是什么?
共 2 条评论2 - 822019-05-28获取锁本身是什么样的操作,怎么保证在这个点上不出现异常呢?
作者回复: 获取锁失败只有一种可能就是mutex对象非法(比如为nil),那就抛出异常,本身也是属于异常安全的代码。
2 - 觉2019-05-24感恩大佬分享 随喜大佬2
- kylexy_08172019-05-24今天刚好阅读到老师写的go语言编程并发的章节,里面说到通过channel进行goroutine间的消息传递,是会阻塞的,所以可以理解成也是锁吗?性能方面并不会比锁好?
作者回复: channel 有锁这是从实现角度来讲,内部用到了锁。channel 是锁,这是从使用界面来讲,这显然是错误的。channel 会阻塞,是因为它实现了生产者消费者模型,如果两边的速度不匹配,自然会出现等待,这本身是一个feature,不是问题。channel 的开销更大,但是说不上谁好或者不好,因为锁和channel适用的场景几乎没有重叠。
2