Java转Go—17锁

在并发编程中,永远离不开的就是多个线程并发操作同一个资源的安全性,在go语言中也一样,如果有多个goroutine并发操作同一资源,就需要加锁控制并发的安全性。

代码示例:

package main

import (
 "fmt"
 "time"
)

var sum = 0

func main() {
 go add()
 go add()
 time.Sleep(time.Second)
 fmt.Println(sum)
}

func add() {
 for i := 0; i < 10000; i++ {
  sum += 1
 }
}

假设有以上代码,在该代码中,定义了一个add函数,在该函数内对全局变量sum进行一万次加 1 ,然后在 main 函数中启动两个 goroutine 调用该函数,则理想结果下是全局变量在每一个 goroutine 中加一万次,两个 goroutine 就一共加了20000次,所以最后的 sum 的结果就应该是20000,但是在上面的代码中最后得到的 sum 结果并不是每一次都是 20000, 这就是在并发编程中出现的访问临界资源(上面代码中的临界资源就是全局变量 sum )的安全性问题。

运行结果:

Java转Go—17锁
image-20220130200026866

如果需要保证上面的代码在多个 goroutine 访问临界资源时的安全性,就需要使用到并发编程中的锁。

在 go 语言的并发编程中,用于保证并发安全性常用的锁有互斥锁和读写锁。

互斥锁

互斥锁是一种常用的保证并发安全的锁,使用互斥锁可以保证在同一时间只能有一个 goroutine 能够访问共享资源,在 go 语言中使用 sync 包下的 Mutex 来实现互斥锁。

互斥锁定义:

var lock sync.Mutex

加锁:

lock.Lock()

解锁:

lock.Unlock()

使用互斥锁修改上面代码,使得代码能够正常运行。

package main

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

var sum = 0
var lock sync.Mutex   // 定义互斥锁

func main() {
 go add()
 go add()
 time.Sleep(time.Second)
 fmt.Println(sum)
}

func add() {
 for i := 0; i < 10000; i++ {
  lock.Lock()   // 加锁
  sum += 1
  lock.Unlock()  // 解锁
 }
}

上述代码使用了互斥锁,首先定义一个互斥锁 lock ,然后在 add 方法循环中进行累加之前和之后分别进行加锁和解锁,这样就可以保证在同一时刻永远只会有一个 goroutine 进行累加,其他的 goroutine 等待,当一个 goroutine 执行完并解锁之后另一个 goroutine 才会继续加锁并执行,这样就不会出现同时多个 goroutine 累加导致最后结果与预期不一致的问题。代码经过这样修改之后运行得到的 sum 就永远都是20000。

读写锁

互斥锁是完全互斥的,在实际的开发过程中,可能会出现某个 goroutine 仅仅只是读取共享资源的值,并不会对资源进行修改,这样的话使用互斥锁会降低代码的运行性能,所以这时候就需要读写锁,读写锁就是在读取资源时不会对其加锁,而是在修改资源的时候才会对其加锁。

假设有两个 goroutine ,分别是 A 和 B

  • 如果 A 获取到读写锁的读锁,B 再获取读锁会直接获得,不需要等待 A 解锁。
  • 如果 A 获取到读写锁的读锁,B 再获取写锁就会等待。
  • 如果 A 获取到读写锁的写锁,B 再获取写锁就会等待。
  • 如果 A 获取到读写锁的写锁,B 再获取读锁就会等待。

代码示例:

package main

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

var x = 0
var rwLock sync.RWMutex
var wg sync.WaitGroup

func main() {
 for i := 0; i < 10; i++ {
  wg.Add(1)
  go read()
 }
 for i := 0; i < 10; i++ {
  wg.Add(1)
  go write()
 }
 wg.Wait()
}

// 读操作
func read() {
 rwLock.RLock()         // 加读锁
 fmt.Println(x)         // 读操作
 rwLock.RUnlock()        // 解锁
 wg.Done()
}

func write() {
 rwLock.Lock()         // 加写锁
 x += 1           // 写操作
 time.Sleep(time.Second)       // 假设写操作需要 1 秒
 rwLock.Unlock()         // 解锁
 wg.Done()
}

在上面代码中,定义两个函数,分别是读函数 read 和写函数 write ,在 read 函数中进行读锁的加锁和解锁,在 write 函数中进行写锁的加锁和解锁,然后在 main 函数中分别启动 10 个 goroutine 执行 read 和 write 函数,根据最后打印的时间间隔可以发现,如果是获取到了读锁,则再次获取读锁就会很快打印出结果,如果是再次获取写锁则会等待读锁解锁之后才会获取成功。


原文始发于微信公众号(良猿):Java转Go—17锁

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/215791.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!