go语言学习笔记25------读写锁、互斥锁

1锁

为了解决协程同步的问题我们使用了channel,但是GO也提供了传统的同步工具。

它们都在GO的标准库代码包sync和sync/atomic中。

下面我们看一下锁的应用。

什么是锁呢?就是某个协程(线程)在访问某个资源时先锁住,防止其它协程的访问,等访问完毕解锁后其他协程再来加锁进行访问。

1.1死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。

死锁:是 “锁”的一种错误使用状态!!!—— 编程过程中,应“提早”避免。

常见的死锁:

1)同一个goroutine中,使用同一个 channel 读写。

2) 2个 以上的go程中, 使用同一个 channel 通信。 读写channel 先于 go程创建。

3) 2个以上的go程中,使用多个 channel 通信。 A go 程 获取channel 1 的同时,尝试使用channel 2, 同一时刻, 
B go 程 获取channel 2 的同时,尝试使用channel 1 

4)在go语言中, channel 和 读写锁、互斥锁 尽量避免交叉混用。——“隐形死锁”。如果必须使用。推荐借助“条件变量”

1.2互斥锁

每个资源都对应于一个可称为 “互斥锁” 的标记,这个标记用来保证在任意时刻,只能有一个协程(线程)访问该资源。其它的协程只能等待。

互斥锁是传统并发编程对共享资源进行访问控制的主要手段,它由标准库sync中的Mutex结构体类型表示。sync.Mutex类型只有两个公开的指针方法,Lock和Unlock。Lock锁定当前的共享资源,Unlock进行解锁。

在使用互斥锁时,一定要注意:对资源操作完成后,一定要解锁,否则会出现流程执行异常,死锁等问题。通常借助defer。锁定后,立即使用defer语句保证互斥锁及时解锁。如下所示:

var mutex sync.Mutex // 定义互斥锁变量 mutex

func write(){
mutex.Lock( )
defer mutex.Unlock( )
}

我们可以使用互斥锁来解决任务编程的问题,如下所示:

package main

import (
   "sync"
   "fmt"
   "time"
)

var mut sync.Mutex

func printer(str string) {
   //mut.Lock()
   //defer mut.Unlock()
   for _, data := range str {
      fmt.Printf("%c", data)
   }
   fmt.Println()
}
func person1() {
   printer("hello")
}
func person2() {
   printer("world")
}
func main() {
   go person1()
   person2()
time.Sleep(time.Second)
}
//输出结果
//worhello
//ld

加上互斥锁上

package main

import (
   "sync"
   "fmt"
   "time"
)

var mut sync.Mutex

func printer(str string) {
   mut.Lock()
   defer mut.Unlock()
   for _, data := range str {
      fmt.Printf("%c", data)
   }
   fmt.Println()
}
func person1() {
   printer("hello")
}
func person2() {
   printer("world")
}
func main() {
   go person1()
   person2()
time.Sleep(time.Second)
}
//输出结果
//world
//hello

程序执行结果与多任务资源竞争时一致。最终由于添加了互斥锁,可以按序先输出hello再输出 world。但这里需要我们自行创建互斥锁,并在适当的位置对锁进行释放。

1.3读写锁

互斥锁的本质是当一个goroutine访问的时候,其他goroutine都不能访问。这样在资源同步,避免竞争的同时也降低了程序的并发性能。程序由原来的并行执行变成了串行执行。

其实,当我们对一个不会变化的数据只做“读”操作的话,是不存在资源竞争的问题的。因为数据是不变的,不管怎么读取,多少goroutine同时读取,都是可以的。

所以问题不是出在“读”上,主要是修改,也就是“写”。修改的数据要同步,这样其他goroutine才可以感知到。所以真正的互斥应该是读取和修改、修改和修改之间,读和读是没有互斥操作的必要的。

因此,衍生出另外一种锁,叫做读写锁。

读写锁可以让多个读操作并发,同时读取,但是对于写操作是完全互斥的。也就是说,当一个goroutine进行写操作的时候,其他goroutine既不能进行读操作,也不能进行写操作。

GO中的读写锁由结构体类型sync.RWMutex表示。此类型的方法集合中包含两对方法:

一组是对写操作的锁定和解锁,简称“写锁定”和“写解锁”:
func (*RWMutex)Lock()
func (*RWMutex)Unlock()

另一组表示对读操作的锁定和解锁,简称为“读锁定”与“读解锁”:
func (*RWMutex)RLock()
func (*RWMutex)RUnlock()

读写锁基本示例:

package main

import (
   "sync"
   "math/rand"
   "time"
   "fmt"
)

var count int

var mutex sync.RWMutex

func write(n int) {
   rand.Seed(time.Now().UnixNano())
   fmt.Printf("写 goroutine %d 正在写数据...\n", n)
   mutex.Lock()
   num := rand.Intn(500)
   count = num
   fmt.Printf("写 goroutine %d 写数据结束,写入新值 %d\n", n, num)
   mutex.Unlock()

}
func read(n int) {
   mutex.RLock()
   fmt.Printf("读 goroutine %d 正在读取数据...\n", n)
   num := count
   fmt.Printf("读 goroutine %d 读取数据结束,读到 %d\n", n, num)
   mutex.RUnlock()
}
func main() {
   for i := 0; i < 10; i++ {
      go read(i + 1)
   }
   for i := 0; i < 10; i++ {
      go write(i + 1)
   }
   time.Sleep(time.Second*5)
}
//输出结果
////读 goroutine 1 正在读取数据...
////读 goroutine 1 读取数据结束,读到 0
////读 goroutine 7 正在读取数据...
////读 goroutine 7 读取数据结束,读到 0
////读 goroutine 3 正在读取数据...
////读 goroutine 3 读取数据结束,读到 0
////读 goroutine 10 正在读取数据...
////读 goroutine 10 读取数据结束,读到 0
////读 goroutine 8 正在读取数据...
////读 goroutine 8 读取数据结束,读到 0
////读 goroutine 6 正在读取数据...
////读 goroutine 5 正在读取数据...
////读 goroutine 5 读取数据结束,读到 0
////写 goroutine 2 正在写数据...
////读 goroutine 4 正在读取数据...
////读 goroutine 4 读取数据结束,读到 0
////写 goroutine 4 正在写数据...
////写 goroutine 3 正在写数据...
////读 goroutine 2 正在读取数据...
////读 goroutine 2 读取数据结束,读到 0
////写 goroutine 9 正在写数据...
////读 goroutine 6 读取数据结束,读到 0
////写 goroutine 7 正在写数据...
////读 goroutine 9 正在读取数据...
////读 goroutine 9 读取数据结束,读到 0
////写 goroutine 6 正在写数据...
////写 goroutine 1 正在写数据...
////写 goroutine 8 正在写数据...
////写 goroutine 10 正在写数据...
////写 goroutine 5 正在写数据...
////写 goroutine 2 写数据结束,写入新值 365
////写 goroutine 4 写数据结束,写入新值 47
////写 goroutine 3 写数据结束,写入新值 468
////写 goroutine 9 写数据结束,写入新值 155
////写 goroutine 7 写数据结束,写入新值 112
////写 goroutine 6 写数据结束,写入新值 490
////写 goroutine 1 写数据结束,写入新值 262
////写 goroutine 8 写数据结束,写入新值 325
////写 goroutine 10 写数据结束,写入新值 103
////写 goroutine 5 写数据结束,写入新值 353

我们在read里使用读锁,也就是RLock和RUnlock,写锁的方法名和我们平时使用的一样,是Lock和Unlock。这样,我们就使用了读写锁,可以并发地读,但是同时只能有一个写,并且写的时候不能进行读操作。

我们从结果可以看出,读取操作可以并行,例如2,3,1正在读取,但是同时只能有一个写,例如1正在写,只能等待1写完,这个过程中不允许进行其它的操作。

处于读锁定状态,那么针对它的写锁定操作将永远不会成功,且相应的Goroutine也会被一直阻塞。因为它们是互斥的。

猜你喜欢

转载自blog.csdn.net/weixin_42927934/article/details/82533940