每个资源都对应于一个可称为 "互斥锁" 的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待。
互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。
在使用互斥锁时,一定要注意:对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题。通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。
var mt sync.Mutex mt.Lock() // 加锁 ... // 共享资源 mt.Unlock() // 解锁
package main import ( "sync" "time" ) import "fmt" var mt sync.Mutex func test(str string) { mt.Lock() for _, s := range str{ //fmt.Println(s) // 打印的是字节 fmt.Printf("%c",s) time.Sleep(time.Millisecond * 100) } mt.Unlock() } func foo(str string) { test(str) } func fun(str string) { test(str) } func main() { go foo("hello") go fun("world") for { ; } }
互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行
当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的,因为读不会对数据进行更改,问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。
因此,衍生出另外一种锁,叫做读写锁。
读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。
GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:
一组是对写操作的锁定和解锁,简称“写锁定”和“写解锁”:
func (*RWMutex)Lock() func (*RWMutex)Unlock()
另一组表示对读操作的锁定和解锁,简称为“读锁定”与“读解锁”:
func (*RWMutex)RLock() func (*RWMutex)RUlock()
案例:
var rwMut sync.RWMutex var value int func readGo(i int) { for{ rwMut.RLock() // 加读锁 num:= value fmt.Printf("%d th , read %d \n", i, num) rwMut.RUnlock() // 解读锁 } } func writeGo(i int, qu chan <- int) { for q := 1;q < 500; q++{ if q == 10{ qu <- 1 } num := rand.Intn(1000) rwMut.Lock() // 加写锁 value = num fmt.Printf("%d th , write %d \n", i, num) time.Sleep(time.Millisecond * 100) rwMut.Unlock() // 解写锁 } } func main() { quit := make(chan int) rand.Seed(time.Now().UnixNano()) for i:=1;i < 5 ;i++ { go readGo(i+1) } for i:=1;i < 5 ;i++ { go writeGo(i+1, quit) } <- quit } >>> : 3 th , write 466 // 写独占 4 th , read 466 // 读共享 2 th , read 466 3 th , read 466 5 th , read 466 4 th , write 646 4 th , read 646 3 th , read 646 2 th , read 646 5 th , read 646 5 th , write 319 ....