多线程会遇到的问题
临界资源的安全问题
临界资源:指并发环境中多个进程、线程、协程共享的资源
在并发编程中对临界资源的处理不当,往往会导致数据不一致的问题。
//
func main() {
// 临界资源:多个协程共享的变量,会导致程序结果位置
a := 1
go func() {
a = 2
fmt.Println("goroutine a:", a)
}()
a = 3
time.Sleep(3 * time.Second)
fmt.Println("main a:", a)
}
售票问题
并发本身并不复杂,但是因为有了资源竞争的问题,就使得我们开发出好的并发程序变得复杂起来,因为会引起很多莫名其妙的问题。
如果多个goroutine在访问同一个数据资源的时候,其中一个线程修改了数据,那么这个数值就被修改了,对于其他的goroutine来讲,这个数值可能是不对的。
package main
import (
"fmt"
"time"
)
// 定义全局变量 票库存为10张
var ticket int = 10
func main() {
// 单线程不存在问题,多线程资源争抢就出现了问题
go saleTickets("张三")
go saleTickets("李四")
go saleTickets("王五")
go saleTickets("赵六")
time.Sleep(time.Second * 5)
}
// 售票函数
func saleTickets(name string) {
for {
if ticket > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", ticket)
ticket--
} else {
fmt.Println("票已售完")
break
}
}
}
发现结果和预想的不同,多线程加入之后,原先单线程的逻辑出现了问题。
出现了临界资源安全问题。
sync包 - 锁
要想解决临界资源安全的问题,很多编程语言的解决方案都是同步。通过上锁的方式,某一时间段,只能允许一个goroutine来访问这个共享数据,当前goroutine访问完毕, 解锁后,其他的goroutine才 能来访问。
我们可以借助于sync包下的锁操作。 synchronization
但是实际上,在Go的并发编程中有一句很经典的话:不要以共享内存的方式去通信:锁,而要以通信的方式去共享内存。
共享内存的方式
锁:多个线程拿的是同一个钥匙,go语言不建议使用锁机制来解决。不要以共享内存的方式去通信
而要以通信的方式去共享内存 go语言更建议我们使用 chan 来解决安全问题。(后面会学)
在Go语言中并不鼓励用锁保护共享状态的方式,在不同的Goroutine中分享信息(以共享内存的方式去通信)。而是鼓励通过channeI将共享状态或共享状态的变化在各个Goroutine之间传递(以通信的方式去共享内存) ,这样同样能像用锁一样保证在同一的时间只有一个Goroutine访问共享状态。
当然,在主流的编程语言中为了保证多线程之间共享数据安全性和一致性,都会提供一套基本的同步工具集,如锁,条件变量,原子操作等等。Go语言标准库也毫不意外的提供了这些同步机制,使用方式也和其他语言也差不多。
package main
import (
"fmt"
"sync"
"time"
)
// 定义全局变量 票库存为10张
var ticket int = 10
// 定义一个锁 Mutex 锁头
var mutex sync.Mutex
func main() {
// 单线程不存在问题,多线程资源争抢就出现了问题
go saleTickets("张三")
go saleTickets("李四")
go saleTickets("王五")
go saleTickets("赵六")
time.Sleep(time.Second * 5)
}
// 售票函数
func saleTickets(name string) {
for {
// 在拿到共享资源之前先上锁
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Millisecond * 1)
fmt.Println(name, "剩余票的数量为:", ticket)
ticket--
} else {
// 操作完毕后,解锁
mutex.Unlock()
fmt.Println("票已售完")
break
}
// 操作完毕后,解锁
mutex.Unlock()
}
}
同步等待组
package main
import (
"fmt"
"sync"
"time"
)
// waitgroup、
var wg sync.WaitGroup
func main() {
// wg.Add(2) 判断还有几个线程、计数 num=2
// wg.Done() 我告知我已经结束了 -1
wg.Add(2)
go test1()
go test2()
fmt.Println("main等待ing")
wg.Wait() // 等待 wg 归零,才会继续向下执行
fmt.Println("end")
// 理想状态:所有协程执行完毕之后,自动停止。
//time.Sleep(3 * time.Second)
}
func test1() {
for i := 0; i < 10; i++ {
time.Sleep(3 * time.Second)
fmt.Println("test1--", i)
}
wg.Done()
}
func test2() {
defer wg.Done()
for i := 0; i < 10; i++ {
fmt.Println("test2--", i)
}
}
不要以共享内存的方式去通信,而要以通信的方式去共享内存。