【Go并发编程】通讯机制与锁

110 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 9 天,点击查看活动详情

传统的编程语言(比如:C++、Java、Python 等)并非面向并发而生的,所以他们面对并发的逻辑多是基于操作系统的线程。并发的执行单元(线程)之间的通信,利用的也是操作系统提供的线程或进程间通信的原语,比如:共享内存、信号(signal)、管道(pipe)、消息队列、套接字(socket)等。

在这些通信原语中,使用最多、最广泛的(也是最高效的)是结合了线程同步原语(比如:锁以及更为低级的原子操作)的共享内存方式,因此,我们可以说传统语言的并发模型是基于对内存的共享的。

CSP

CSP(Communicating Sequential Processes,通信顺序进程)

Tony Hoare 的 CSP 模型旨在简化并发程序的编写,让并发程序的编写与编写顺序程序一样简单。Tony Hoare 认为输入输出应该是基本的编程原语,数据处理逻辑(也就是 CSP 中的 P)只需调用输入原语获取数据,顺序地处理数据,并将结果数据通过输出原语输出就可以了。

一个符合 CSP 模型的并发程序应该是一组通过输入输出原语连接起来的 P 的集合。 在 Go 中,与“Process”对应的是 goroutine

image.png

channel

goroutine 可以从 channel 获取输入数据,再将处理后得到的结果数据通过 channel 输出。通过 channel 将 goroutine(P)组合连接在一起,让设计和编写大型并发系统变得更加简单和清晰

sync 包

互斥锁(sync.Mutex)

  • mutex不是可重入锁
  • 保证 Lock/Unlock 成对出现,尽可能采用 defer mutex.Unlock 的方式,把它们成对、紧凑地写在一起。
// 创建
var loc sync.Mutex
loc := sync.Mutex{}
// 加锁
loc.Lock()
// 解锁
loc.Unlock()
// 非阻塞加锁
loc.TryLock()
var mut sync.Mutex
sum := 0
for i := 0; i < 5000; i++ {
   go func() {
      defer mut.Unlock()
      mut.Lock() //加锁
      sum++
   }()
}

嵌入字段,在struct 上直接调用 Lock/Unlock 方法。还可以把获取锁、释放锁、计数加一的逻辑封装成一个方法。

// 线程安全的计数器类型
type Counter struct {
    CounterType int
    Name        string
    mu    sync.Mutex
    count uint64
}

// 加1的方法,内部使用互斥锁保护
func (c *Counter) Incr() {
    c.mu.Lock()
    c.count++
    c.mu.Unlock()
}

// 得到计数器的值,也需要锁保护
func (c *Counter) Count() uint64 {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.count
}

读写锁(sync.RWMutex)

读时并发,写时加锁

var mut sync.RWMutex
// 写锁
mut.Lock()
mut.Unlock()
mut.TryLock()

// 读锁
mut.RLock()
mut.RUnlock()
mut.TryRLock()

// 获得一个读锁
mut.RLocker()

sync.WaitGroup

wait时所有Add都Done后才结束阻塞。

var wg sync.WaitGroup
// 增加计数
wg.Add(2)
// 减少计数
wg.Done()
// 阻塞直到wg为0
wg.Wait()
  • 注意所有add完成,再运行wait
  • wait时不要再并发add

条件变量(sync.Cond)

一个条件变量可以理解为一个容器,这个容器中存放着一个或一组等待着某个条件成立的goroutine。当条件成立时,这些处于等待状态的goroutine将得到通知并被唤醒以继续后续的工作。

c := sync.NewCond(&sync.Mutex{})
// 加锁更改等待条件
c.L.Lock()
c.L.Unlock()
// 等待唤醒
c.Wait()
// 广播唤醒所有的等待者
c.Signal()
c.Broadcast()
  • 调用 cond.Wait 方法之前一定要加锁
  • waiter goroutine 被唤醒不等于等待条件被满足

Cond 和一个 Locker 关联,可以利用这个 Locker 对相关的依赖条件更改提供保护。

Once

可以多次调用 Do 方法,但是只有第一次调用 Do 方法时 f 参数才会执行,这里的 f 是一个无参数无返回值的函数。

var once sync.Once
f1 := func() {
   fmt.Println("in f1")
}
once.Do(f1) 

Once实现单例模式

type Singleton struct {
}

var singleInstance *Singleton
var once sync.Once

func GetSingletonObj() *Singleton {
   once.Do(func() {
      singleInstance = new(Singleton)
   })
   return singleInstance
}