go从0到1项目实战体系九:锁机制

Golang中的锁机制主要包含互斥锁和读写锁.

1. 为什要加锁?

①. 现实场景:
   a. 厕所是共享的,你要上厕所,就需要先开门再锁上锁.就是一个加锁的操作.
   b. 红绿灯也是一种资源的共享,红灯就表示上锁,不能通行.

②. 同理,线程是共享的,所有要加锁.
   a. 如果map和数组被多个goroute来修改,如果不加锁就有异常.
   b. 多个协程同时读一个变量的值没有问题,是不用加锁的.
   c. 一个变量同时读写也要加锁.
   d. 发生共享的值?
      1. 一个是全局变量.
      2. 也可能map是通过参数传进来的.

③. 互斥锁 => 读和写频率一样的场景
   读写锁 => 读多写少的场景

(1). 在并发情况下,多个线程或协程同时修改一个变量,不加锁的结果:

func main() {
  var a = 0
  for i := 0; i < 1000; i++ {
    go func(idx int) {
      a += 1
      fmt.Println(a)
    }(i)
  }
  time.Sleep(time.Second)      // 等待1s结束主程序,确保所有协程执行完
}:. 从理论上,函数里每次递增a的值的,应该有1000个不同的值输出.. 执行结果:
   $ go run test.go |sort|uniq |wc -l
   998       // 绝对出现了重复值
   $ go run test.go |sort|uniq |wc -l
   1000
   a. 打印的值不是按顺序来的,因为是协程在执行.. 协程依次执行顺序:
   从寄存器读取a的值 -> 再做加法运算 -> 最后写到寄存器.. 并发场景:
   a. 一个协程取出a的值是3,正在做加法运算(还未写回寄存器).
   b. 同时另一个协程去取,取出了同样的a的值3.
   c. 最终,两个协程产出的结果相同,a相当于只增加了1.. 互斥锁是传统并发程序对共享资源进行控制访问的主要手段.
   a. 在Go中主要使用sync.Mutex的结构体表示.
   b. 互斥锁应该是成对出现,同级别互斥锁不能嵌套使用,不可以再对锁加锁.
   c. 也不可以多次对一个锁解锁.. 只有一个能连进去,它释放后,下一个才能进去,性能比较低.
   a. 因为只要无论是读或写,都上会锁,只有一个才能进去.

(2). 加锁:

①. 有多个线程要同时操作一个资源.如果不加锁,是不是别人也可以进去.

②. 在并发的情况下,多个线程或协程同时去修改一个变量:
   a. 使用锁能保证在某一时间点内,只有一个协程或线程修改这一变量.
   b. 如:我正在处理a(锁定),谁都别和我抢,等我处理完了(解锁),你们再处理.
      => 这样就实现了,同时处理a的协程只有一个,就实现了同步.

③. 如果要避免红绿灯,就要架设高架桥.后果是共享的资源变成了独占的资源了,就不用加锁了.

2. 加锁:

sync包里提供了Locker接口、互斥锁Mutex、读写锁RWMutex用于处理并发过程中可能出现同时两个或多个协程(或线程)读或写同一个变量的情况.

2.1 互斥锁Mutex:

import("sync")
func main() {
  var a = 0
  var lock sync.Mutex
  for i := 0; i < 1000; i++ {
    go func(idx int) {
      lock.Lock()
      // defer lock.Unlock()      // 加了这句话,下面的Unlock就不用加
      a += 1
      lock.Unlock()
      fmt.Printf("goroutine %d, a=%d\n", idx, a)
    }(i)
  }
  time.Sleep(time.Second)
}:. 执行的结果总是1000个不重复的值,但是打印的值不是按顺序来的.. 一个互斥锁只能同时被一个goroutine锁定,其它goroutine将阻塞直到互斥锁被解锁(重新争抢对互斥锁的锁定).

(1). 证明:

func main() {
  ch := make(chan struct{}, 2)
  var l sync.Mutex
  go func() {
      l.Lock()
      defer l.Unlock()
      fmt.Println("goroutine1: 我会锁定大概2s")
      time.Sleep(time.Second * 2)
      fmt.Println("goroutine1: 我解锁了,你们去抢吧")
      ch <- struct{}{}
  }()
  go func() {
      fmt.Println("groutine2: 等待解锁")
      l.Lock()
      defer l.Unlock()
      fmt.Println("goroutine2: 我也解锁了")
      ch <- struct{}{}
  }()
  // 等待 goroutine 执行结束
  for i := 0; i < 2; i++ {
      <-ch
  }
}:
groutine2: 等待解锁
goroutine1: 我会锁定大概2s
goroutine1: 我解锁了,你们去抢吧
goroutine2: 我也解锁了

2.2 读写锁:

(1). 4个方法:

. 写操作的锁定和解锁:
   func (*RWMutex) Lock
   func (*RWMutex) Unlock

②. 读操作的锁定和解锁:
   func (*RWMutex) Rlock
   func (*RWMutex) RUnlock

(2) 理解:

①. 规则:
   a. 当有一个goroutine获得写锁定,其它无论是读锁还是写锁都将阻塞直到写解锁.
   b. 当有一个goroutine获得读锁定,其它读锁定仍然可以继续.
   c. 当有一个或任意多个读锁定,写锁定将等待所有读锁定解锁之后才能够进行写锁定.
      1. 所以这里的读锁定(RLock),目的是告诉写锁定,有很多人正在读取数据,你不能操作.
      2. 等它们读解锁完了,你再来写锁定.

②. 读写锁可以分别针对读操作和写操作进行分别锁定,有一定的性能提升:
   a. 对于多个写操作,以及写操作和读操作之间是互斥的.
   b. 对于同时多个读操作之前却非互斥关系,这是读写锁性能高于互斥锁的主要原因.

③. 总结:
   a. 同时只能有一个goroutine能够获得写锁定.
   b. 同时可以有任意多个gorouinte获得读锁定.
   c. 同时只能存在写锁定或读锁定(读和写互斥).

④. 举例:
   a. 教室里面的黑板,全班的同学都可以读黑板,但只能老师才能够改变黑板上面的文字.
   b. 如果有两个老师同时更改黑板的内容,会造成同学们无法确定该读谁写的文字.
   c. 其中,一个老师在写文字的时候,需要把这一过程上锁,一旦上了读写锁.
   d. 第二个老师只能等待第一个老师写完才可以进行书写.

(3). 读写锁共存:

var count int
var rw sync.RWMutex
func main() {
    ch := make(chan struct{}, 10)
    for i := 0; i < 5; i++ {
        go read(i, ch)
    }
    for i := 0; i < 5; i++ {
        go write(i, ch)
    }
    for i := 0; i < 10; i++ {
        <-ch
    }
}
func read(n int, ch chan struct{}) {
    rw.RLock()
    fmt.Printf("goroutine %d 进入读操作...\n", n)
    v := count
    fmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)
    rw.RUnlock()
    ch <- struct{}{}
}
func write(n int, ch chan struct{}) {
    rw.Lock()
    fmt.Printf("goroutine %d 进入写操作...\n", n)
    v := rand.Intn(1000)
    count = v
    fmt.Printf("goroutine %d 写入结束,新值为:%d\n", n, v)
    rw.Unlock()
    ch <- struct{}{}
}

结果:
goroutine 1 进入读操作...
goroutine 1 读取结束,值为:0
goroutine 4 进入读操作...
goroutine 4 读取结束,值为:0
goroutine 2 进入读操作...
goroutine 2 读取结束,值为:0           // 在上面过程中,读都是0
goroutine 4 进入写操作...
goroutine 4 写入结束,新值为:81        // 在写操作之后,值是81
goroutine 0 进入读操作...
goroutine 0 读取结束,值为:81
goroutine 3 进入读操作...
goroutine 3 读取结束,值为:81
goroutine 0 进入写操作...
goroutine 0 写入结束,新值为:887
goroutine 1 进入写操作...
goroutine 1 写入结束,新值为:847
goroutine 2 进入写操作...
goroutine 2 写入结束,新值为:59
goroutine 3 进入写操作...
goroutine 3 写入结束,新值为:81

(4). 只有读锁:

var m *sync.RWMutex
func main() {
    m = new(sync.RWMutex)
    // 多个同时读
    go read(1)
    go read(2)
    time.Sleep(2*time.Second)
}
func read(i int) {
    println(i,"read start")
    m.RLock()
    println(i,"reading")
    time.Sleep(1*time.Second)
    m.RUnlock()
    println(i,"read over")
}

结果:
1 read start
1 reading
2 read start
2 reading
1 read over
2 read over
多个读操作同时读一个操作,虽然加了锁,但都是读是不受影响的.

(5). 读写锁:

var m *sync.RWMutex
func main() {
	m = new(sync.RWMutex)
	// 写的时候啥也不能干
	go write(1)
	go read(2)
	go write(3)
	time.Sleep(2*time.Second)
}
func read(i int) {
	println(i,"read start")
	m.RLock()
	println(i,"reading")
	time.Sleep(1*time.Second)
	m.RUnlock()
	println(i,"read over")
}
func write(i int) {
	println(i,"write start")
	m.Lock()
	println(i,"writing")
	time.Sleep(1*time.Second)
	m.Unlock()
	println(i,"write over")
}

结果:
2 read start
2 reading
3 write start
1 write start
2 read over
3 writing

你可能感兴趣的:(golang,开发语言,后端)