对Go中通过共享内存实现通信的深度理解| 青训营笔记

86 阅读3分钟

这是我参与「第五届青训营」笔记创作活动的第4天。

一、内容:

通过前几天对Go并发编程的学习,了解到Go中的CSp主要有两种:

通过通信实现共享内存、通过共享内存实现通信

前面的笔记里有了对第一种CSP模型比较详细的记载,但没有展开讲述第二种CSP模型。故在今天更加充分的去学习了第二种CSP模型,以及Go中常见锁的相关知识。在此写下经过这段学习后我对第二种CSP模型--通过共享内存实现通信的理解

image.png

二、临界区

在go语言中,经常会遇到并发的问题,当然我们会优先考虑使用通道,同时go语言也提供了传统解决方式Mutex(互斥锁)和RWMutex(读写锁)来处理竞争问题。发生并发问题的原因在于并行操作了共同的资源,发生了资源竞争。这些修改公共资源的代码被称为临界区

这里使用银行存款问题来演示:

type Bank struct {
	balance int //余额
}

func (b *Bank) deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done()
	b.balance += amount
}

func (b *Bank) getBalance() int {
	return b.balance
}

func main() {
	b := new(Bank)
	var wg sync.WaitGroup
	wg.Add(1000)
	for i := 0; i < 1000; i++ {
		go b.deposit(1, &wg)
	}
	wg.Wait()
	fmt.Println(b.getBalance())
}
//生成不同的结果 如 987

这里的balence就是临界区

三、互斥锁和读写锁

可以使用互斥锁或读写锁解决这个问题:

使用互斥锁来解决这个问题:

type Bank struct {
	balance int //余额
	m       sync.Mutex
}

func (b *Bank) deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done()
	b.m.Lock()
	b.balance += amount
	b.m.Unlock()
}

但是,互斥锁会导致同一时间只有一个程序可以执行加锁后的程序,对于读操作较多的业务不是很友好,此时可以使用读写锁。

type Bank struct {
	balance int //余额
	m       sync.RWMutex
}

func (b *Bank) deposit(amount int, wg *sync.WaitGroup) {
	defer wg.Done()
	defer b.m.Unlock()
	b.m.Lock()
	b.balance += amount
}

func (b *Bank) getBalance() (balance int) {
	b.m.RLock()
	balance = b.balance
	b.m.RUnlock()
	return  //默认返回blance
}

我们将互斥锁改为读写锁,并改变读取函数,从而实现读写锁。

读写锁与互斥锁的区别:

Mutex 是最简单的一种锁类型,同时也比较暴力,当一个 goroutine 获得了 Mutex 后,其他 goroutine 就只能乖乖等到这个 goroutine 释放该 Mutex。

RWMutex--读写锁,顾名思义是一把锁分为两部分:读锁和写锁,其中读锁允许多个线程同时获得,因为读操作本身是线程安全的,而写锁则是互斥锁,不允许多个线程同时获得写锁,并且写操作和读操作也是互斥的。总结来说,读写锁的特点是:读读不互斥、读写互斥、写写互斥。

主要遵循以下规则 :

  1. 读写锁的读锁可以重入,在已经有读锁的情况下,可以任意加读锁。
  2. 在读锁没有全部解锁的情况下,写操作会阻塞直到所有读锁解锁。
  3. 写锁定的情况下,其他协程的读写都会被阻塞,直到写锁解锁。

四、总结:

今天通过这些例子对临界区、锁、共享内存实现通信有了进一步的理解。

五、引用参考: