网站地图    收藏   

主页 > 后端 > Golang >

golang的并发读写变量报错之处理锁,通道,原子操作

来源:未知    时间:2023-04-20 17:02 作者:小飞侠 阅读:

[导读] 并发编程   Golang并发通过 goroutine 来实现。goroutine是由Go语言的运行时#xff08;runtime#xff09;调度完成的#xff0c;而线程是由操作系统调度完成。 goroutine 和 channel 是Go秉承的CSP(Communicating Seque...

并发编程

  Golang并发通过goroutine来实现。goroutine是由Go语言的运行时(runtime)调度完成的,而线程是由操作系统调度完成。goroutinechannel是Go秉承的CSP(Communicating Sequential Process)并发模式的重要实现基础。

1. 协程(goroutine)

  Go中使用goroutine非常简单,只需要在调用函数时在前面加上go关键字。一个goroutine必定对应一个函数,可以创建多个goroutine去执行相同的函数。

  OS线程(操作系统线程)一般都有固定的栈内存(通常为2MB),一个goroutine的栈在其生命周期开始时只有很小的栈(典型情况下2KB),goroutine的栈不是固定的,他可以按需增大和缩小,goroutine的栈大小限制可以达到1GB,虽然极少会用到这么大。所以在Go语言中一次创建十万左右的goroutine也是可以的。
  参考自https://www.liwenzhou.com/posts/Go/14_concurrence/

1.1. 创建一个goroutine

  下面的代码开启一个单独的goroutine来执行hello函数。结果只打印了main,是因为执行hello的goroutine还没来得及调度,main函数就已经终止了。

package main 
import ("fmt")
func hello() {
		fmt.Println("hello")
}
func main() {
		go hello()//time.Sleep(time.Second)
		fmt.Println("main")
}

  如何将上述hello()函数执行呢?一种直接的方法,使用time.Sleep(),让main函数所在的goroutine休眠一段时间。但是在实际的业务逻辑中, 这样的方式实现goroutine同步,绝对是一种憨憨做法。于是,便有了一种优雅的方式来等待goroutine的结束,即sync.WaitGroup

1.2. sync.WaitGroup

  使用sync.WaitGroup的难点在于确定goroutine是什么时候结束的,即在哪里调用wg.Done()让计数器减一。

  1. 下面我开启了100个goroutine,来执行hello()中的任务,为了使得0-99个goroutine调度顺序依次执行,我开启每个goroutine以后,休眠1ms再开启下一个goroutine

package mainimport ("fmt""time")func hello(i int) {
		fmt.Println("hello goroutine", i)}func main() {for i := 0; i < 100; i++ {go hello(i)
			time.Sleep(time.Millisecond)}}

   如果一个goroutine花费0.1ms就可以调度完成&#xff0c;这里我给每个goroutine分配1ms就很不合理&#xff0c;而且每个goroutine有可能执行时间不一样&#xff0c;那么我是否可以让一个goroutine里的任务一致性完&#xff0c;就调度另一个goroutine而不是这种sleep的憨憨做法。

  1. 使用sync.WaitGroup同步方式

  通常情况下&#xff0c;会使用sync.WaitGroup的方式实现多个goroutine之间的同步。

package mainimport ("fmt""sync")func hello(i int) {defer wg.Done() // goroutine将hello中任务执行完&#xff0c;计数器减一
		fmt.Println("hello goroutine", i)}var wg sync.WaitGroupfunc main() {for i := 0; i < 100; i++ {
			wg.Add(1) // 每开启一个goroutine,计数器加一go hello(i)//time.Sleep(time.Millisecond)
			wg.Wait() // 等待计数器降为0&#xff0c;再继续执行}// 更多的业务场景是&#xff0c;多个goroutine和main之间的同步&#xff0c;上述代码只是为了模拟sleep同样的效果。}

2. channel

  单纯的将函数并发执行是没有意义的&#xff0c;函数和函数之间需要交换数据才能体现并发的价值。我们可以使用共享内存的方式进行数据交换&#xff08;通过共享内存实现通信&#xff09;&#xff0c;但是它会导致不同的goroutine中发生竞态问题。为了保证数据交换的正确性&#xff0c;必须使用互斥量对内存进行加锁&#xff0c;这种做法会导致性能问题。
  Golang并发模型&#xff08;CSP : Communicating Sequential Processes&#xff09;&#xff0c;提倡通过通信共享内存而不是通过共享内存而实现通信。channel可以让一个goroutine发送特定值到另一个goroutine&#xff0c;可以看作是一种通信机制。

2.1. channel类型

// 1. 声明var ch1 chan intvar ch2 chan bool// 2. 创建channelmake(chan int, 16)// 3. 将一个值发送到通道
	ch <- 10// 4. 从一个通道中接收值
	x := <- ch// 赋值给x<- ch// 忽略结果// 5. 关闭通道close(ch)

2.2. 优雅的操作channel

  1. 循环读取数据

package mainimport "fmt"func main() {
		ch1 := make(chan int)
		ch2 := make(chan int)// 开启一个goroutine&#xff0c;把0-24之间的数发送到ch1go func() {for i := 0; i < 25; i++ {
				ch1 <- i}close(ch1)}()// 从ch1中取出数据&#xff0c;计算平方&#xff0c;放到ch2go func() {for {
				x, ok := <- ch1if ok {
					ch2 <- x * x}}close(ch2)}()for i := range ch2 {
			fmt.Println(i)}}

2.3. 单向通道

  有些通道可能只用于发送&#xff0c;或者只用于接收。单向通道多用于函数的参数中。

func f1(ch1 chan<- int) {}func f2(ch1 <-chan int){}

2.4. 通道注意事项

关闭&#xff1a; 只有在通知接收方goroutine所有的数据都发送完毕的时候才需要关闭通道。通道是可以被GC回收的&#xff0c;它和关闭文件是不一样的&#xff0c;在结束操作之后关闭文件是必须要作的&#xff0c;但关闭通道不是必须的。

2.5. 生产者消费者模型案例

  开启两个协程&#xff0c;一个协程生产数据&#xff0c;另一个协程对数据进行处理&#xff0c;处理完再把数据发回去。

package mainimport ("fmt""sync")var wg sync.WaitGroupfunc producer(in chan<- int) {defer wg.Done()for i := 0; i < 10; i++ {
			in <- i * i}close(in)}func consumer(out <-chan int) {defer wg.Done()for {
			x, ok := <-outif !ok {break}
			fmt.Println("num: ", x)}}func main() {
		ch := make(chan int, 16)
		wg.Add(2)go producer(ch)go consumer(ch)
		wg.Wait()
		fmt.Println("主协程结束...")}

3. 锁

3.1. 竞态

  竞态是指在多个goroutine按某些交错顺序执行时&#xff0c;程序无法给出正确的结果。它对于程序是致命的&#xff0c;因为它们潜伏在程序中&#xff0c;出现的频率也很低&#xff0c;有可能仅在高负载环境或者在特定的编译器、平台和架构时才会出现。发生竞态现象的必要条件是&#xff1a;两个goroutine并发读写同一个变量&#xff0c;并且至少其中一个goroutine是写入操作。

  在存款操作和取款操作并发执行的时候&#xff0c;就会发生静态现象。有三种方法可以避免数据静态。

  1. 不要修改变量。

  2. 避免从多个goroutine访问同一变量。

  3. 允许多个goroutine访问同一变量&#xff0c;但同一时间只有一个goroutine可以访问。这种方法称为互斥机制。

  Golang中代码中加锁&#xff0c;一般体现在是对临界区加锁。

   临界区&#xff1a;程序片段访问临界资源的代码&#xff0c;临界区同一时刻只能有一个线程运行&#xff0c;其他线程必须等待访问&#xff0c;所以需要用到锁&#xff1b;

3.2. 互斥锁&#xff1a;sync.Mutex

  互斥锁是一种常用的控制共享资源访问的方法&#xff0c;它能够保证同时只有一个goroutine可以访问共享资源。Go语言中使用sync包的Mutex类型实现互斥锁。
  使用互斥锁能够保证同一时间只有一个goroutine进入临界区&#xff0c;其他的goroutine则在等待锁&#xff1b;当互斥锁释放后&#xff0c;等待的goroutine才可以获取锁进入临界区&#xff0c;多个goroutine同时等待一个锁时&#xff0c;唤醒的策略是随机的。

3.3. 读写互斥锁&#xff1a;sync.RWMutex

  互斥锁是完全互斥的&#xff0c;但是有很多实际的场景下是读多写少&#xff08;数据库读写分离&#xff09;&#xff0c;当我们并发的去读取一个资源不涉及资源修改的时候&#xff0c;没必要加锁&#xff0c;这种情况下使用读写锁是更好的一种选择。读写锁在Go语言中使用sync包中的RWMutex类型。
  读写锁分为两种&#xff1a;读锁和写锁。当一个goroutine获取读锁之后&#xff0c;其他的goroutine如果获取的是读锁无需等待&#xff0c;如果是获取写锁就会等待&#xff1b;当一个goroutine获取写锁之后&#xff0c;其他的goroutine无论是获取读锁还是写锁都会等待。

4. sync包其它应用

4.1. sync.Once

  在编程的中&#xff0c;我们需要确保某些操作在高并发的场景下只执行一次&#xff0c;例如只加载一次配置文件、只关闭一次通道等。Golang中sync包提供了一个解决方案sync.Once

// 如果要执行函数f,就需要搭配闭包来使用func (o *Once) Do(f func()) {}

4.2. sync.Map

  Go语言内置的map不是并发安全的&#xff0c;多个goroutine并发访问map时会发生错误&#xff08;一般超过21个goroutine读写map时编译器就会报错&#xff09;。可以使用互斥锁的方式访问临界资源&#xff08;在操作map前加锁&#xff0c;操作完释放锁&#xff09;。由于Golang针对map的使用场景较多&#xff0c;系统提供了开箱即用&#xff08;不需要使用make初始化&#xff09;的并发安全的map即 sync.Map

5. atomic原子性操作

  1. 我开10万个goroutine&#xff0c;每个goroutine对全局变量x加一&#xff0c;每次执行结果都不一样。

package mainimport ("fmt""sync")var x intvar wg sync.WaitGroupfunc add() {
		x++
		wg.Done()}func main() {for i := 0; i < 100000; i++ {
			wg.Add(1)go add()}
		wg.Wait()
		fmt.Println(x)}

//输出 97357
2. 加锁(sync.Mutex)

package mainimport ("fmt""sync")var x intvar wg sync.WaitGroupvar lock sync.Mutexfunc add() {defer wg.Done()// 对临界区加锁
	lock.Lock()
	x++
	lock.Unlock()}func main() {for i := 0; i < 100000; i++ {
		wg.Add(1)go add()}
	wg.Wait()
	fmt.Println(x)}

// 输出10000
3. 使用原子操作

package mainimport ("fmt""sync""sync/atomic")var x int64var wg sync.WaitGroupvar lock sync.Mutexfunc add() {defer wg.Done()//lock.Lock()//x++//lock.Unlock()
		atomic.AddInt64(&x, 1)}func main() {for i := 0; i < 100000; i++ {
			wg.Add(1)go add()}
		wg.Wait()
		fmt.Println(x)}

image.png

以上就是golang的并发读写变量报错之处理锁,通道,原子操作全部内容,感谢大家支持自学php网。

自学PHP网专注网站建设学习,PHP程序学习,平面设计学习,以及操作系统学习

京ICP备14009008号-1@版权所有www.zixuephp.com

网站声明:本站所有视频,教程都由网友上传,站长收集和分享给大家学习使用,如由牵扯版权问题请联系站长邮箱904561283@qq.com

添加评论