Golang并发安全

并发介绍

并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任一个时刻点上只有一个程序在处理机上运行。
总的来说,并发的优点还是有很多的,如可以同时处理多个请求、响应更快等等,当然最为致命的缺点是其安全性存在漏洞。比如以下实例:银行账户进行取钱时,会先向后台发送请求,确保该账户有足够的钱可以取出,这之间会产生时间差。当两人同时对同一账户进行取钱时,假如时间相差甚少,可能会跳过后台进行检索的时间,这时两人就取出了两倍的钱,而账户只扣除一份的钱。这就是多个线程共享数据时,可能会产生于期望不相符的结果。

以银行账户为例

(不加互斥锁)代码如下:

package main

import (
	"fmt"
	"time"
)
//银行账户
type Account struct {
	money int
}
//存钱
func (a *Account) SaveMoney(c int) {
	fmt.Println("存钱开始")
	a.money += c
	fmt.Println("存钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
}
//取钱
func (a *Account) GetMoney(c int) {
	fmt.Println("取钱开始")
	a.money -= c
	fmt.Println("取钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
}
//查询
func (a *Account) QueryMoney()  {
	fmt.Println("当前余额为:",a.money)
}
func main() {
	acc := Account{100}
	for i := 0; i < 3; i++ {
		go acc.GetMoney(10)
		go acc.SaveMoney(10)
	}
	time.Sleep(5e9)
	acc.QueryMoney()
}

运行程序时,3次取钱和3次存钱同时打印,意味着使用并发时,对同一份数据进行存钱与取钱的操作,可能会产生预期之外的结果。因此要考虑进行存钱或取钱时,尽管使用并发,仍然能控制执行操作的协程只为1个。互斥锁正是起到这般效果。

(加互斥锁)代码如下:

package main

import (
	"fmt"
	"time"
)

var(
	//互斥锁的使用:
	// 进入子协程后的第一步:mt.Lock
	// 退出子协程前的最后一步:mt.Unlock
	mt sync.Mutex
)
//银行账户
type Account struct {
	money int
}
//存钱
func (a *Account) SaveMoney(c int) {
	mt.Lock()
	fmt.Println("存钱开始")
	a.money += c
	fmt.Println("存钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
	mt.Unlock()
}
//取钱
func (a *Account) GetMoney(c int) {
	mt.Lock()
	fmt.Println("取钱开始")
	a.money -= c
	fmt.Println("取钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
	mt.Unlock()
}
//查询
func (a *Account) QueryMoney()  {
	fmt.Println("当前余额为:",a.money)
}
func main() {
	acc := Account{100}
	for i := 0; i < 3; i++ {
		go acc.GetMoney(10)
		go acc.SaveMoney(10)
	}
	time.Sleep(5e9)
	acc.QueryMoney()
}

运行程序时,每次打印时,只有1次取钱或存钱打印,避免了同时取钱和存钱的操作。

细节

互斥锁的细节:
进入子协程后的第一步:mt.Lock
退出子协程前的最后一步:mt.Unlock

错误代码:

func (a *Account) GetMoney(c int) {
	fmt.Println("取钱开始")
	a.money -= c
	fmt.Println("取钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
}

func main() {
	acc := Account{100}
	for i := 0; i < 3; i++ {
	    mt.Lock()
		go acc.GetMoney(10)
		mt.Unlock()
	}
	time.Sleep(5e9)
	fmt.Println(acc)
}

当主协程进到for循环中,先执行的mt.Lock()是没有意义的,因为此时还没有开出子协程。

正确代码:

func (a *Account) GetMoney(c int) {
	mt.Lock()
	fmt.Println("取钱开始")
	a.money -= c
	fmt.Println("取钱结束")
	//此处的time.Sleep休眠是为了直观看出存钱与取钱是互斥的
	time.Sleep(1e9)
	mt.Unlock()
}

func main() {
	acc := Account{100}
	for i := 0; i < 3; i++ {
		go acc.GetMoney(10)
	}
	time.Sleep(5e9)
	fmt.Println(acc)
}

当主协程进到for循环中,执行go acc.GetMoney(10),即开出子协程实现acc.GetMoney(10)方法,在进入acc.GetMoney(10)的第一步先锁住协程,确保只有一个子协程执行后续步骤,这样就避免有多个协程同时进行操作。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值