许式伟的架构课
许式伟
七牛云CEO
立即订阅
19948 人已学习
课程目录
已更新 71 讲 / 共 77 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 怎样成长为优秀的软件架构师?
免费
基础平台篇 (21讲)
01 | 架构设计的宏观视角
02 | 大厦基石:无生有,有生万物
03 | 汇编:编程语言的诞生
04 | 编程语言的进化
05 | 思考题解读:如何实现可自我迭代的计算机?
06 | 操作系统进场
07 | 软件运行机制及内存管理
08 | 操作系统内核与编程接口
09 | 外存管理与文件系统
10 | 输入和输出设备:交互的演进
11 | 多任务:进程、线程与协程
12 | 进程内协同:同步、互斥与通讯
13 | 进程间的同步互斥、资源共享与通讯
14 | IP 网络:连接世界的桥梁
15 | 可编程的互联网世界
16 | 安全管理:数字世界的守护
17 | 架构:需求分析 (上)
18 | 架构:需求分析 (下) · 实战案例
19 | 基础平台篇:回顾与总结
加餐 | 我看Facebook发币(上):区块链、比特币与Libra币
加餐 | 我看Facebook发币(下):深入浅出理解 Libra 币
桌面开发篇 (16讲)
20 | 桌面开发的宏观视角
21 | 图形界面程序的框架
22 | 桌面程序的架构建议
23 | Web开发:浏览器、小程序与PWA
24 | 跨平台与 Web 开发的建议
25 | 桌面开发的未来
26 | 实战(一):怎么设计一个“画图”程序?
27 | 实战(二):怎么设计一个“画图”程序?
28 | 实战(三):怎么设计一个“画图”程序?
29 | 实战(四):怎么设计一个“画图”程序?
30 | 实战(五):怎么设计一个“画图”程序?
31 | 辅助界面元素的架构设计
课外阅读 | 从《孙子兵法》看底层的自然法则
加餐 | 想当架构师,我需要成为“全才”吗?
32 | 架构:系统的概要设计
33 | 桌面开发篇:回顾与总结
服务端开发篇 (14讲)
34 | 服务端开发的宏观视角
35 | 流量调度与负载均衡
36 | 业务状态与存储中间件
37 | 键值存储与数据库
38 | 文件系统与对象存储
39 | 存储与缓存
40 | 服务端的业务架构建议
41 | 实战(一):“画图”程序后端实战
42 | 实战(二):“画图”程序后端实战
43 | 实战(三):“画图”程序后端实战
44 | 实战(四):“画图”程序后端实战
45 | 架构:怎么做详细设计?
46 | 服务端开发篇:回顾与总结
加餐 | 如何做HTTP服务的测试?
服务治理篇 (11讲)
47 | 服务治理的宏观视角
48 | 事务与工程:什么是工程师思维?
49 | 发布、升级与版本管理
50 | 日志、监控与报警
加餐 | 怎么保障发布的效率与质量?
51 | 故障域与故障预案
52 | 故障排查与根因分析
53 | 过载保护与容量规划
54 | 业务的可支持性与持续运营
55 | 云计算、容器革命与服务端的未来
56 | 服务治理篇:回顾与总结
架构思维篇 (8讲)
57 | 心性:架构师的修炼之道
用户故事 | 站在更高的视角看架构
58 | 如何判断架构设计的优劣?
59 | 少谈点框架,多谈点业务
60 | 架构分解:边界,不断重新审视边界
加餐 | 实战:“画图程序” 的整体架构
61 | 全局性功能的架构设计
62 | 重新认识开闭原则 (OCP)
许式伟的架构课
登录|注册

12 | 进程内协同:同步、互斥与通讯

许式伟 2019-05-24
你好,我是七牛云许式伟。
上一讲开始我们进入了多任务的世界,我们详细介绍了三类执行体:进程、线程和协程,并且介绍了每一种执行体的特点。
既然启动了多个执行体,它们就需要相互协同,今天我们先讨论进程内的执行体协同。
考虑到进程内的执行体有两类:用户态的协程(以 Go 语言的 goroutine 为代表)、操作系统的线程,我们对这两类执行体的协同机制做个概要。如下:
让我们逐一详细分析一下它们。

原子操作

首先让我们看一下原子操作。需要注意的是,原子操作是 CPU 提供的能力,与操作系统无关。这里列上只是为了让你能够看到进程内通讯的全貌。
顾名思义,原子操作的每一个操作都是原子的,不会中途被人打断,这个原子性是 CPU 保证的,与执行体的种类无关,无论 goroutine 还是操作系统线程都适用。
从语义上来说,原子操作可以用互斥体来实现,只不过原子操作要快得多。
例如:
var val int32
...
newval = atomic.AddInt32(&val, delta)
等价于:
var val int32
var mutex sync.Mutex
...
mutex.Lock()
val += delta
newval = val
mutex.Unlock()

执行体的互斥

互斥体也叫锁。锁用于多个执行体之间的互斥访问,避免多个执行体同时操作一组数据产生竞争。其使用界面上大概是这样的:
func (m *Mutex) Lock()
func (m *Mutex) Unlock()
锁的使用范式比较简单:在操作需要互斥的数据前,先调用 Lock,操作完成后就调用 Unlock。但总是存在一些不求甚解的人,对锁存在各种误解。
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《许式伟的架构课》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(49)

  • 笨拙的自由
    希望老师类比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();
             }
        }
    }

    2019-05-24
    26
  • 立耳
    许老师,下面的这段代码是不是存在问题,以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 的次序可以随意交换。

    2019-05-27
    10
  • Geek_gooy
    老师
    我明白了, 比如A线程notify或者signal,被唤醒的线程并不会马上执行,而是需要等待A线程退出同步块或者unlock才会执行。

    如果是notifyAll,也同样如此,但是等到唤醒并获得执行权的线程执行结束后,CPU会优先把执行权交给上次唤醒没有得到执行权的某个线程,而不会给阻塞在锁外面等待锁的线程。和调用notify只唤醒一个还是有些许区别的。

    作者回复: 是这样,你可以看我文章中Wait的代码,在唤醒后第一件事情是lock,也就是请求锁,所以只有A线程unlock后,其他被唤醒的线程中的一个会得到锁往下走。

    2019-05-26
    7
  • 小美
    协程和线程还是没区别清楚

    作者回复: 一个操作系统调度,一个用户态自己来调度

    2019-05-24
    5
  • Cordova
    看完代码后发现go做的通信发现并没有什么优势,其他语言做通信也这么干、刷了下评论区提到libevent、才恍然发现标题这节讲的是同步!可能是我太期待许老师讲异步了😂~ 目前python我用异步首选会把异步过程交给libuv。听了老师上节讲到python的协程只是一种编程范式,想到内置的asyncio虽然是做了异步但还是有很较大的性能提升空间这个逻辑也就通了!希望许老师在讲异步的时候能多提一提跨平台异步库他们是怎么实现

    作者回复: 1、go和java的代码只是形似,实质不同,因为go里面的channel是协程的通讯设施,java版本的是线程的通讯设施,大相径庭;
    2、我们本节提的同步,和同步io的同步,两个是完全不同含义的同步;
    3、我们课程不太会讲资料已经相对多的某个细节,除非这个细节非常关键影响到全局的理解。

    2019-05-28
    4
  • Geek_gooy
    老师
    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、对的

    2019-05-25
    3
  • 白小狮
    go中的panic会导致整个主进程都挂掉,goroutine里面的panic后cover不住,是逻辑上就是应该整个主进程都退出吗

    作者回复: 设计上,goroutine 应该自己去 recover 错误,而不是主进程来 recover。

    2019-05-25
    3
  • 杨雪峰
    感觉和 Java 的 wait notify notifyall 很像
    2019-05-24
    3
  • Ender
    还是没太明白条件变量在channel代码里面的意义,所有操作都是先获取锁,在一个操作没完成的情况下其他都不会进到cond.Wait()呀。按理只需要锁就能做到了channel的实现了。

    作者回复: 向 channel.push 一个对象时,要考虑 channel 满了,这时会等待,这就是 cond.Wait 的逻辑

    2019-06-06
    2
  • Geek_03056e
    将线程统一申请,弄一个线程池,使用条件变量唤醒,使用时唤醒,用完后休眠,就像nginx中的线程池。这种设计是不和协程就一样了,在用户态构建执行体?

    作者回复: 不一样,这样写最终用的是libevent的异步回调模式。没法用同步io模型。

    2019-05-26
    2
  • cc
    老师,我看你的chanel代码实现,比如pop方法。是先broadcast,后pop。我的理解刚好相反。应该是先pop后通知。请教下问什么

    作者回复: 次序可以交换的。因为反正锁还没有释放,这里只是标记一下哪些执行体可以调度

    2019-05-25
    2
  • Taozi
    总算明白为什么叫条件变量了,拿这里的Channel 实现来说,几个执行体要读写的队列是“变量”,队列的长度是“等待条件”和“唤醒条件”。是这样理解吗?

    作者回复: 对的

    2019-05-25
    2
  • 輪迴
    同时在看《深入浅出计算机组成原理》和《GO 语言从入门到实战》,发现三个课程之间的关联性还是蛮多的,相辅相成,更加帮助知识点的深入理解

    作者回复: 👍

    2019-05-24
    2
  • 感恩大佬分享 随喜大佬
    2019-05-24
    2
  • thewangzl
    老师你好,文章中说Signal比Broadcast好些,但是王宝令老师的专栏《Java并发编程实战》第15章提到Dubbo唤醒等待线程从signal优化为signalAll。是因为java中的signal/signalAll和Go的Signal/Broadcast有差异吗?

    作者回复: 你说的是对的。我其实没想到什么情况下用signal,大部分情况下都是用 broadcast,包括本文中的例子。因为用 signal 意味着每次资源的使用都要通知,其实退化为信号量的 PV 操作了,这一定性能是变差的。

    2019-05-29
    1
  • Bachue Zhou
    管道合理的使用场景是什么?难道不是父子间进程通讯或是子进程之间的通讯吗?进程间通讯可不是 Goroutine 能轻易取代的。

    作者回复: Go语言里面有两个管道实现,一个io.Pipe,是用于goroutine之间的;一个是os.Pipe,是用于进程之间的。

    2019-05-28
    1
  • Bachue Zhou
    看不懂为什么要把管道的使用场景放在 Goroutine 之间通讯,这并不是管道合理的使用场景啊,至于和 Channel 做对比就更没有意义了。

    作者回复: 管道合理的使用场景是什么?

    2019-05-28
    1
  • 82
    在高并发下获取锁操作,谁来保证单次操作的原子性,操作系统还是cpu或者其他呢?

    作者回复: 当然是锁。如果这都保证不了,那它就不是锁了

    2019-05-28
    1
  • 82
    获取锁本身是什么样的操作,怎么保证在这个点上不出现异常呢?

    作者回复: 获取锁失败只有一种可能就是mutex对象非法(比如为nil),那就抛出异常,本身也是属于异常安全的代码。

    2019-05-28
    1
  • keshawn
    Channel只是一个并发阻塞队列?

    作者回复: 本质上是的,不过它有一些高级用法是常规队列没有的,比如用select去多个channel同时进行读写。

    2019-05-27
    1
收起评论
49
返回
顶部