Golang面试前二夜准备
题号 | 题目 |
16 | 怎么查看Goroutine的数量 |
17 | Go中的锁有哪些 |
16. 怎么查看Goroutine的数量
在Golang中,GOMAXPROCS中控制的是未被阻塞的所有Goroutine,可以被 Multiplex 到多少个线程上运行,通过NumGoroutine可以查看Goroutine的数量。
package main import ( "fmt" "runtime" ) func main() { fmt.Println(runtime.GOMAXPROCS(10)) //GOMAXPROCS设置可以执行的cpu的最大数量 fmt.Println(runtime.NumGoroutine()) // NumGoroutine返回当前存在的goroutine数量。 }
17. Go中的锁有哪些
Go中的三种锁包括: 互斥锁
,读写锁
,sync.Map的安全的锁.
- 互斥锁
Go并发程序对共享资源进行访问控制的主要手段,由标准库代码包中sync中的Mutex结构体表示。
看看sync.Mutex的声明:
// Mutex 是互斥锁, 零值是解锁的互斥锁, 首次使用后不得复制互斥锁。 type Mutex struct { state int32 sema uint32 }
sync.Mutex包中的类型只有两个公开的指针方法Lock和Unlock:
// Locker表示可以锁定和解锁的对象。 type Locker interface { Lock() Unlock() } // 锁定当前的互斥量 // 如果锁已被使用,则调用goroutine // 阻塞直到互斥锁可用。 func (m *Mutex) Lock() // 对当前互斥量进行解锁 // 如果在进入解锁时未锁定m,则为运行时错误。 // 锁定的互斥锁与特定的goroutine无关。 // 允许一个goroutine锁定Mutex然后安排另一个goroutine来解锁它。 func (m *Mutex) Unlock()
声明一个互斥锁:
var mutex sync.Mutex
不像C或Java的锁类工具,我们可能会犯一个错误:忘记及时解开已被锁住的锁,从而导致流程异常。但Go由于存在defer,所以此类问题出现的概率极低。关于defer解锁的方式如下:
var mutex sync.Mutex func Write() { mutex.Lock() defer mutex.Unlock() }
注意:
- 如果对一个已经上锁的对象再次上锁,那么就会导致该锁定操作被阻塞,直到该互斥锁回到被解锁状态.
- 互斥锁锁定操作的逆操作并不会导致协程阻塞,但是有可能导致引发一个无法恢复的运行时的panic,比如对一个未锁定的互斥锁进行解锁时就会发生panic。避免这种情况的最有效方式就是使用defer。
- 我们知道如果遇到panic,可以使用recover方法进行恢复,但是如果对重复解锁互斥锁引发的panic却是无用的,即异常会继续抛出来(Go 1.8及以后)。
- 虽然互斥锁可以被多个协程共享,但还是建议将对同一个互斥锁的加锁解锁操作放在同一个层次的代码中。
- 读写锁
读写锁是针对读写操作的互斥锁,可以分别针对读操作与写操作进行锁定和解锁操作 。
读写锁的访问控制规则如下:
- 多个写操作之间是互斥的.
- 写操作与读操作之间也是互斥的.
- 多个读操作之间不是互斥的.
在这样的控制规则下,读写锁可以大大降低性能损耗。
在Go的标准库代码包中sync中的RWMutex结构体表示为:
// RWMutex是一个读/写互斥锁,可以由任意数量的读操作或单个写操作持有。 // RWMutex的零值是未锁定的互斥锁。 // 首次使用后,不得复制RWMutex。 // 如果goroutine持有RWMutex进行读取而另一个goroutine可能会调用Lock,那么在释放初始读锁之前,goroutine不应该期望能够获取读锁定。 // 特别是,这种禁止递归读锁定。 这是为了确保锁最终变得可用; 阻止的锁定会阻止新读操作获取锁定。 type RWMutex struct { w Mutex //如果有待处理的写操作就持有 writerSem uint32 // 写操作等待读操作完成的信号量 readerSem uint32 //读操作等待写操作完成的信号量 readerCount int32 // 待处理的读操作数量 readerWait int32 // number of departing readers }
sync中的RWMutex有以下几种方法:
//对读操作的锁定 func (rw *RWMutex) RLock() //对读操作的解锁 func (rw *RWMutex) RUnlock() //对写操作的锁定 func (rw *RWMutex) Lock() //对写操作的解锁 func (rw *RWMutex) Unlock() //返回一个实现了sync.Locker接口类型的值,实际上是回调rw.RLock and rw.RUnlock. func (rw *RWMutex) RLocker() Locker
Unlock方法会试图唤醒所有想进行读锁定而被阻塞的协程,而RUnlock方法只会在已无任何读锁定的情况下,试图唤醒一个因欲进行写锁定而被阻塞的协程。
若对一个未被写锁定的读写锁进行写解锁,就会引发一个不可恢复的panic,同理对一个未被读锁定的读写锁进行读写锁也会如此。
由于读写锁控制下的多个读操作之间不是互斥的,因此对于读解锁更容易被忽视。对于同一个读写锁,添加多少个读锁定,就必要有等量的读解锁,这样才能其他协程有机会进行操作。
因此Go中读写锁,在多个读线程可以同时访问共享数据,写线程必须等待所有读线程都释放锁以后,才能取得锁.
同样的,读线程必须等待写线程释放锁后,才能取得锁,也就是说读写锁要确保的是如下互斥关系,可以同时读,但是读-写,写-写都是互斥的。
- sync.Map安全锁
golang中的sync.Map是并发安全的,其实也就是sync包中golang自定义的一个名叫Map的结构体。
sync.Map结构体声明:
type Map struct { // 该锁用来保护dirty mu Mutex // 存读的数据,因为是atomic.value类型,只读类型,所以它的读是并发安全的 read atomic.Value // readOnly //包含最新的写入的数据,并且在写的时候,会把read 中未被删除的数据拷贝到该dirty中,因为是普通的map存在并发安全问题,需要用到上面的mu字段。 dirty map[interface{}]*entry // 从read读数据的时候,会将该字段+1,当等于len(dirty)的时候,会将dirty拷贝到read中(从而提升读的性能)。 misses int }
read的数据结构是:
type readOnly struct { m map[interface{}]*entry // 如果Map.dirty的数据和m 中的数据不一样是为true amended bool }
entry的数据结构:
type entry struct { //可见value是个指针类型,虽然read和dirty存在冗余情况(amended=false),但是由于是指针类型,存储的空间应该不是问题 p unsafe.Pointer // *interface{} }
方法:
func (m *Map) Delete(key interface{}) func (m *Map) Store(key, value interface{}) func (m *Map) Load(key interface{}) (value interface{}, ok bool)
关于方法源码大家可以自行去研究,由于篇幅关系,这里不做讲解,源码还是挺简单的哈。
sync.Map是通过冗余的两个数据结构(read、dirty),实现性能的提升。
为了提升性能,load、delete、store等操作尽量使用只读的read;为了提高read的key击中概率,采用动态调整,将dirty数据提升为read;对于数据的删除,采用延迟标记删除法,只有在提升dirty的时候才删除。