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

进程的执行体:

用户态的协程(以 Go 语言的 goroutine 为代表)...

操作系统的线程

机制:



原子操作

原子操作是 CPU 提供的能力,与操作系统无关。

代码:

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()


执行前调用锁函数,执行完调用解锁函数

从进程内通讯来说,比锁快的东西,只有原子操作

channel 的每个操作必然是有锁的

channel 的每个操作都比较耗时。

锁的最大问题在于不容易控制。

看这段代码:

mutex.Lock()
doSth()
mutex.Unlock()


异常代码的情况下 这段代码是执行不下去的

GO语言提供了  defer

mutex.Lock()
defer mutex.Unlock()
doSth()


不要在锁里面执行费时操作。

指在mutex.Lock和mutex.Unlock之间的代码


读操作

mutex.RLock()
defer mutex.RUnlock()
doReadOnlyThings

写操作

mutex.Lock()
defer mutex.Unlock()
doWriteThings


读操作不阻止读操作,阻止写操作; 写操作阻止一切,不管读操作...


执行体同步:

等分操作

用法:

var wg WaitGroup
...
wg.Add(n)
for 循环 n 次 {
    go func() {
        defer wg.Done()
        doTaski  // 执行第 i 个任务
    }()
}
wg.Wait()


条件变量的使用:

var mutex sync.Mutex  // 也可以是 sync.RWMutex
var cond = sync.NewCond(&mutex)
...


创建条件变量需要用到读写锁的原因是 cond.Wait 需要

把自己加入到挂起队列
mutex.Unlock()
等待被唤醒  // 挂起的执行体会被后续的 cond.Broadcast 或 cond.Signal() 唤醒
mutex.Lock()


条件变量的标准模板:

mutex.Lock()
defer mutex.Unlock()
for conditionNotMetToDo {
    cond.Wait()
}
doSomething
if conditionNeedNotify {
    cond.Broadcast()
    // 有时可以优化为 cond.Signal()
}


cond.Broadcast 唤起所有挂起的执行体 ond.Signal 则只唤醒其中的一个。

什么时候 怎么用?

挂起在这个条件变量上的执行体,它们等待的条件是一致的;

本次 doSomething 操作完成后,所释放的资源只够一...

变量是指 “一组要在多个执行体之间协同的数据”

。条件是指做任务前 Wait 的 “前置条件”,和做任务时需...

type Channel struct {
    mutex sync.Mutex
    cond *sync.Cond
    queue *Queue
    n int
}

func NewChannel(n int) *Channel {
    if n < 1 {
        panic("todo: support unbuffered channel")
    }
    c := new(Channel)
    c.cond = sync.NewCond(&c.mutex)
    c.queue = NewQueue()
    // 这里 NewQueue 得到一个普通的队列
    // 代码从略
    c.n = n
    return c
}

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
}


执行体通讯

channel 就是管道 具体的使用方法

c := make(chan Type, n) // 创建一个能够传递 Type 类型数据的管道,缓冲大小为 n
...
go func() {
    val := <-c // 从管道读入
}()
...
go func() {
    c <- val // 向管道写入
}()



极客时间版权所有: https://time.geekbang.org/column/article/96994



有疑问、勘误、请您在下方留言,感谢您的支持 ღ( ´・ᴗ・` )!

感谢您阅读,这篇文章归 极客点子版权所有.
如果转载,请注明出处: 极客点子版权所有(/page/947.html) 知识共享许可协议
本网站使用 创作共用 归属 - 非商业用途 - 共享4.0国际许可协议的相同方式 许可.

关于作者:

    作者:

    reverse()

    简介:

    nodejs 全栈工程师 、作家、github 开源爱好者
    研究方向:数据分析、 深度学习、 服务器架构、 系统原理

    著作:

    最爱的编程语言:

    Javascript
    Python