在并发编程中对临界资源的处理不当,往往会导致数据的不一致问题
package main
import (
"fmt"
"time"
)
func main() {
a := 1
go func() {
a = 2
fmt.Println("goroutine", a)
}()
a = 3
fmt.Println("a", a)
time.Sleep(time.Second * 3)
fmt.Println("a1", a)
//结果
//a 3
//goroutine 2
//a1 2
}
火车票售票程序。共有10张票,4个售票口同时出售,如何确保库存正常
package main
import (
"fmt"
"time"
)
var ticket int = 10
func main() {
go sale("售票口1")
go sale("售票口2")
go sale("售票口3")
go sale("售票口4")
time.Sleep(time.Second * 3)
//售票口2 当前剩余: 10
//售票口3 当前剩余: 9
//售票口4 当前剩余: 8
//售票口1 当前剩余: 10
//售票口4 当前剩余: 6
//售票口1 当前剩余: 6
//售票口2 当前剩余: 4
//售票口3 当前剩余: 4
//售票口2 当前剩余: 2
//售票口3 当前剩余: 2
//卖光了
//售票口1 当前剩余: 2
//卖光了
//售票口4 当前剩余: 2
//卖光了
//售票口2 当前剩余: -2
//卖光了
}
func sale(name string) {
for {
if ticket > 0 {
time.Sleep(time.Millisecond * 500)
fmt.Println(name, "当前剩余:", ticket)
ticket--
} else {
fmt.Println("卖光了")
break
}
}
}
多个线程争抢时会出现问题。
sync 包提供了对互斥锁(Mutex)的支持,用于实现多个 goroutines 之间的互斥访问。互斥锁是一种同步原语,可以确保在任何时刻,只有一个 goroutine 能够访问共享资源。
package main
import (
"fmt"
"sync"
"time"
)
var ticket int = 10
var mutex = sync.Mutex{}
func main() {
go sale("售票口1")
go sale("售票口2")
go sale("售票口3")
go sale("售票口4")
time.Sleep(time.Second * 8)
//售票口1 当前剩余: 10
//售票口1 当前剩余: 9
//售票口4 当前剩余: 8
//售票口2 当前剩余: 7
//售票口3 当前剩余: 6
//售票口1 当前剩余: 5
//售票口4 当前剩余: 4
//售票口2 当前剩余: 3
//售票口3 当前剩余: 2
//售票口1 当前剩余: 1
//卖光了
//卖光了
//卖光了
//卖光了
}
func sale(name string) {
for {
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Millisecond * 500)
fmt.Println(name, "当前剩余:", ticket)
ticket--
} else {
mutex.Unlock()
fmt.Println("卖光了")
break
}
mutex.Unlock()
}
}
但是实际上,在GO语言的并发编程中,有一句经单的话:不要以共享内存的方式去通信,而要以通信的方式去共享内存。
在GO语言中,并不鼓励用锁的机制来保护共享状态,在不同的Goroutine中分享信息(以共享内存来通信)。而是鼓励通过channel将共享状态或共享状态的变化在各个Goroutine中之间传递(以通信的方式共享内存)。这样同样能像锁一样,保证同一时间只有一个Goroutine能访问共享状态。
在上面的例子中,我们通过time.sleep()来让主线程等待。这个时间我们不能精准控制。而sync.WaitGroup(通常缩写为 wg)是一种用于等待一组 goroutines 完成执行的同步原语。WaitGroup 通过一个计数器来实现等待,计数器的初始值为 0。每当启动一个新的 goroutine 时,计数器就会递增。当 goroutine 完成时,就会调用 Done 方法将计数器递减。主程序可以调用 Wait 方法来阻塞,直到计数器减至零,表示所有的 goroutines 都已经执行完成。
package main
import (
"fmt"
"sync"
"time"
)
var ticket int = 10
var mutex = sync.Mutex{}
var wg sync.WaitGroup
func main() {
wg.Add(4)
go sale("售票口1")
go sale("售票口2")
go sale("售票口3")
go sale("售票口4")
wg.Wait()
//售票口1 当前剩余: 10
//售票口1 当前剩余: 9
//售票口4 当前剩余: 8
//售票口2 当前剩余: 7
//售票口3 当前剩余: 6
//售票口1 当前剩余: 5
//售票口4 当前剩余: 4
//售票口2 当前剩余: 3
//售票口3 当前剩余: 2
//售票口1 当前剩余: 1
//卖光了
//卖光了
//卖光了
//卖光了
}
func sale(name string) {
for {
mutex.Lock()
if ticket > 0 {
time.Sleep(time.Millisecond * 500)
fmt.Println(name, "当前剩余:", ticket)
ticket--
} else {
mutex.Unlock()
wg.Done()
fmt.Println("卖光了")
break
}
mutex.Unlock()
}
}
通道(Channel)是用于在 goroutines 之间进行通信的一种机制。通道提供了一种安全的数据传输方式,确保数据在发送和接收的过程中不会被竞争条件破坏。通道的主要目的是协调不同 goroutines 之间的执行。
package main
import "fmt"
func main() {
var ch chan bool
ch = make(chan bool)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
ch <- true
}()
data := <-ch
fmt.Println("通道里的值", data)
}
一个通道发送和接收数据,默认是阻塞的。当一个数据被发送到通道时,在发送语句中被阻塞,直到另一个Goroutine从该通道读取数据。
相对地,当从通道读取数据时,读取被阻塞,直到一个Goroutine将数据写入该通道。本身channel就是同步的,意味着同一时间,只能有一条goroutine来操作。
最后:通道是goroutine之间的连接,所以通道的发送和接收必须处在不同的goroutine中。
这些通道的特性是帮助Goroutines有效地进行通信,而无需像使用其他编程语言中非常常见的显式锁或条件变量。
如果创建了chan,没有Goroutine来使用了,则会出现死锁。
使用通道时要考虑的一一个重要因素是死锁。如果Goroutine在一个通道上发送数据,那么预计其他的Goroutine应该接收数据。如果这种情况不发生,那么程序将在运行时出现死锁。
类似地,如果Goroutine正在等待从通道接收数据,那么另一些Goroutine将会在该通道上写入数据,否则程序将会死锁。