GO基础进阶篇 (九)、临界资源安全问题(锁、channel)

临界资源安全问题

在并发编程中对临界资源的处理不当,往往会导致数据的不一致问题

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
}

1.售票问题

火车票售票程序。共有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
		}
	}
}

多个线程争抢时会出现问题。

2.mutex锁

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能访问共享状态。

3. WaitGroup

在上面的例子中,我们通过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()
	}
}

4.channel

通道(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将会在该通道上写入数据,否则程序将会死锁。

你可能感兴趣的:(GO语言从基础到应用,golang,服务器,开发语言)