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