go语言之并发安全和锁

前言

有时候在go语言中存在多个goroutine去竞争同一个资源(临界区)。这种情况会发生竞态问题(数据竞态)。

比如:飞机上火车上、人就好比goroutine,去争夺同一个厕所资源,最好的情况就是一个一个来,但是着急的时候两个人一起上就发生数据竞态。

代码:

package main

import (
	"fmt"
	"sync"
)

var x int64
var wg sync.WaitGroup

func add() {
	defer wg.Done()
	for i := 0; i < 100000; i++ {
		x++
	}
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

结果:

上面的代码是开启了两个goroutine去累加x的值,他两同时去修改和访问x会导致数据竞争,导致结果与预期不符合。

此时我们想到使用互斥锁来解决此问题,保证数据共享的同时又不会竞争资源。

互斥锁

互斥锁是一种常用的控制共享资源的办法,它能够同时保证一个gotroutine可以访问共享资源。go语言中使用sync和metux类型来实现互斥锁。

代码如下:

package main

import (
	"fmt"
	"sync"
)

var x int64
var wg sync.WaitGroup
var lock sync.Mutex

func add() {
	defer wg.Done()
	for i := 0; i < 100000; i++ {
		lock.Lock()
		x++
		lock.Unlock()
	}
}
func main() {
	wg.Add(2)
	go add()
	go add()
	wg.Wait()
	fmt.Println(x)
}

结果如下:

这样使用互斥锁能够保证同一时间有且只有一个goroutine进入临界区,其他的goroutine则在等待锁;当互斥锁释放后,等待的goroutine才可以获取锁进入临界区,多个goroutine同时等待一个锁时,唤醒策略是随机的。

读写互斥锁

互斥锁是完全互斥的,但是有很多实际的场景下是读多写少的,当我们并发的去读取一个资源不涉及资源修改的时候是没有必要加锁的,这种场景下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。

读写锁分为两种:读锁和写锁。当一个goroutine获取读锁之后,其他的goroutine如果获取读锁则会继续获取锁,如果是获取写锁就会等待;当一个goroutine获取写锁之后,其他的goroutine无论是读锁还是写锁都会等待。

 代码展示:

package main

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

var x int64
var wg sync.WaitGroup
var lock sync.Mutex
var rwlock sync.RWMutex

func write() {
	defer wg.Done()
	// lock.Lock() //加互斥锁
	rwlock.Lock() //加写锁
	x++
	time.Sleep(time.Microsecond * 10)
	rwlock.Unlock() //解写锁
	// lock.Unlock() //解互斥锁
}

func read() {
	defer wg.Done()
	// lock.Lock()  //加互斥锁
	rwlock.RLock() //加读锁
	time.Sleep(time.Millisecond)
	rwlock.RUnlock() //解读锁
	// lock.Unlock() //解互斥锁
}

func main() {
	start := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go write()
	}
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go read()
	}
	wg.Wait()
	end := time.Now()
	fmt.Println(end.Sub(start))
}

结果如下:

 上面的是互斥锁的执行时间;下面的是读写锁的执行时间;可以看到读写锁效率提升很多。

但是读写锁适合读多写少的场景,需要注意。

sync.WaitGroup

go语言使用sync.WaitGroup来实现并发同步。主要由三个方法:

wg.Add() 计数器+1;

wg.Done() 计数器-1;

wg.Wait() 等到直到计数器变为0;

sync.WaitGroup内部维护着一个计数器,计数器的值可以增加和减少。例如当我们启动了N 个并发任务时,就将计数器值增加N。每个任务完成时通过调用Done()方法将计数器减1。通过调用Wait()来等待并发任务执行完,当计数器值为0时,表示所有并发任务已经完成。

代码展示:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup

func hello() {
	defer wg.Done()
	fmt.Println("Hello Goroutine!")
}
func main() {
	wg.Add(1)
	go hello() // 启动另外一个goroutine去执行hello函数
	fmt.Println("main goroutine done!")
	wg.Wait()
}

结果显示:

sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案–sync.Once

sync.Once只有一个Do方法,其签名如下:

func (o *Once) Do(f func()) {}

备注:如果要执行的函数f需要传递参数就需要搭配闭包来使用。

sync.Map

go语言内置的map并不是并发安全的,当开启很多goroutine时就会报错;这样的情况就需要为map加锁来保证并发的安全性了,go语言提供了一个开箱即用的并发安全版sync.Map。开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如StoreLoadLoadOrStoreDeleteRange等操作方法。

代码如下:

package main

import (
	"fmt"
	"strconv"
	"sync"
)

var m = sync.Map{}

func main() {
	wg := sync.WaitGroup{}
	for i := 0; i < 30; i++ {
		wg.Add(1)
		go func(n int) {
			key := strconv.Itoa(n)
			m.Store(key, n)
			value, _ := m.Load(key)
			fmt.Printf("k=:%v,v:=%v\n", key, value)
			wg.Done()
		}(i)
	}
	wg.Wait()
}

原子操作

前面通过锁来实现同步操作。而锁机制的底层是基于原子操作的,其一般直接通过CPU指令实现。Go语言中原子操作有内置的标准库sync/atomic提供。

atomic包提供了底层的原子级内存操作,对于同步算法的实现很有用。这些函数必须谨慎地保证正确使用。除了某些特殊的底层应用,使用通道或者sync包的函数/类型实现同步更好。

代码展示:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
)

//原子操作
var x int64
var wg sync.WaitGroup
var lock sync.Mutex

// func add() {
// 	defer wg.Done()
// 	lock.Lock()
// 	x++
// 	lock.Unlock()
// }
func add() {
	defer wg.Done()
	atomic.AddInt64(&x, 2)
}
func main() {
	wg.Add(100000)
	for i := 0; i < 100000; i++ {
		go add()
	}
	wg.Wait()
	fmt.Println(x)
}

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值